itsi 0.2.21.rc1 → 0.2.21.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de46fbc801d723fe981489c7170e18d344b7f0de59b340c1c053bd89d880e3be
4
- data.tar.gz: 3b07a649c4de609d4e022962e7745788bebda8a07819bf930dbefea8f5fdebd4
3
+ metadata.gz: b4117986c1d041e81b0ffd2aa390c19db3798893d98c30ed9c3fe51d8fb5dafd
4
+ data.tar.gz: b3025a1c7e133a6afedda5cbe45e92f043c36a30aa3d0e62c325040b9a055d06
5
5
  SHA512:
6
- metadata.gz: daee3821c95ff7a9de766eca58fb92a207ee26f9d6f7526da73159474e5b598ba6ccd5776b432736cb67ab79a9d394853f180cd09796d9874cd9491cfd5c34cc
7
- data.tar.gz: 3c744080b6f29042b572aedb37c0d7c5a6ed30eb83e9a93db2646d802637a50380c335f9bd30be3c3a48ade597c2bc1cdf26d171d36ee5eed590f02a7dc762a7
6
+ metadata.gz: 0e27ce25615c7528cc2732c42bbe1966de03b31c91633ddfbbcf0d42bf439bff56c8878bb631dfd3644115309149dfc69e7ac1cf32b059d9f429b9819dd77893
7
+ data.tar.gz: f6f30ce1824850bc24109ac052613f63f43afaf300ee75280d8a2ad24501998cfe2486594f79b5b272bcc53459b823457c103ef32dfa5c59a82e71dab0a1a838
data/.dockerignore ADDED
@@ -0,0 +1,3 @@
1
+ target
2
+ gems/scheduler/target
3
+ gems/server/target
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## [0.2.21-rc2] - 2026-02-16
2
+ - Fix Ruby 2.7 startup NameError in static_assets redirect schema load path
3
+
1
4
  ## [0.2.21-rc1] - 2026-01-08
2
5
  - Removed Rust `target-cpu=native` to avoid illegal instruction on older CPUs.
3
6
 
data/Cargo.lock CHANGED
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1644
1644
 
1645
1645
  [[package]]
1646
1646
  name = "itsi-scheduler"
1647
- version = "0.2.20"
1647
+ version = "0.2.21-rc1"
1648
1648
  dependencies = [
1649
1649
  "bytes",
1650
1650
  "derive_more",
@@ -1662,7 +1662,7 @@ dependencies = [
1662
1662
 
1663
1663
  [[package]]
1664
1664
  name = "itsi-server"
1665
- version = "0.2.20"
1665
+ version = "0.2.21-rc1"
1666
1666
  dependencies = [
1667
1667
  "argon2",
1668
1668
  "async-channel",
@@ -1933,9 +1933,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
1933
1933
 
1934
1934
  [[package]]
1935
1935
  name = "magnus"
1936
- version = "0.7.1"
1936
+ version = "0.8.2"
1937
1937
  source = "registry+https://github.com/rust-lang/crates.io-index"
1938
- checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab"
1938
+ checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
1939
1939
  dependencies = [
1940
1940
  "bytes",
1941
1941
  "magnus-macros",
@@ -1946,9 +1946,9 @@ dependencies = [
1946
1946
 
1947
1947
  [[package]]
1948
1948
  name = "magnus-macros"
1949
- version = "0.6.0"
1949
+ version = "0.8.0"
1950
1950
  source = "registry+https://github.com/rust-lang/crates.io-index"
1951
- checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
1951
+ checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
1952
1952
  dependencies = [
1953
1953
  "proc-macro2",
1954
1954
  "quote",
@@ -2581,18 +2581,18 @@ dependencies = [
2581
2581
 
2582
2582
  [[package]]
2583
2583
  name = "rb-sys"
2584
- version = "0.9.111"
2584
+ version = "0.9.124"
2585
2585
  source = "registry+https://github.com/rust-lang/crates.io-index"
2586
- checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af"
2586
+ checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
2587
2587
  dependencies = [
2588
2588
  "rb-sys-build",
2589
2589
  ]
2590
2590
 
2591
2591
  [[package]]
2592
2592
  name = "rb-sys-build"
2593
- version = "0.9.111"
2593
+ version = "0.9.124"
2594
2594
  source = "registry+https://github.com/rust-lang/crates.io-index"
2595
- checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29"
2595
+ checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
2596
2596
  dependencies = [
2597
2597
  "bindgen",
2598
2598
  "lazy_static",
@@ -2605,9 +2605,9 @@ dependencies = [
2605
2605
 
2606
2606
  [[package]]
2607
2607
  name = "rb-sys-env"
2608
- version = "0.1.2"
2608
+ version = "0.2.3"
2609
2609
  source = "registry+https://github.com/rust-lang/crates.io-index"
2610
- checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
2610
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
2611
2611
 
2612
2612
  [[package]]
2613
2613
  name = "rcgen"
@@ -2999,9 +2999,9 @@ dependencies = [
2999
2999
 
3000
3000
  [[package]]
3001
3001
  name = "serde_magnus"
3002
- version = "0.9.0"
3002
+ version = "0.11.0"
3003
3003
  source = "registry+https://github.com/rust-lang/crates.io-index"
3004
- checksum = "51b8b945a2dadb221f1c5490cfb411cab6c3821446b8eca50ee07e5a3893ec51"
3004
+ checksum = "8ff64c88ddd26acdcad5a501f18bcc339927b77b69f4a03bfaf2a6fc5ba2ac4b"
3005
3005
  dependencies = [
3006
3006
  "magnus",
3007
3007
  "serde",
data/Dockerfile ADDED
@@ -0,0 +1,9 @@
1
+ FROM ruby:3.4
2
+
3
+ RUN apt-get update && apt-get install build-essential libclang-dev -y && apt-get clean && rm -rf /var/lib/apt/lists/*
4
+ RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
5
+
6
+ COPY pkg/itsi-server-0.2.21.rc1.gem .
7
+ RUN gem install itsi-server-0.2.21.rc1.gem
8
+
9
+ CMD ["itsi", "serve"]
@@ -5,7 +5,7 @@ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  thiserror = "2.0.11"
8
- magnus = { version = "0.7.1" }
8
+ magnus = { version = "0.8.2" }
9
9
  rcgen = "0.13.2"
10
10
  nix = "0.29.0"
11
11
  httparse = "1.10.1"
@@ -5,7 +5,7 @@ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  cfg-if = "1.0.0"
8
- magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
8
+ magnus = { version = "0.8.2", features = ["rb-sys", "bytes"] }
9
9
  nix = "0.29.0"
10
- rb-sys = "0.9.105"
10
+ rb-sys = "0.9.117"
11
11
  serde = "1.0.219"
@@ -1,7 +1,7 @@
1
- use magnus::IntoValue;
2
1
  use magnus::rb_sys::AsRawValue;
3
2
  use magnus::value::BoxValue;
4
- use magnus::{Ruby, Value, value::ReprValue};
3
+ use magnus::IntoValue;
4
+ use magnus::{value::ReprValue, Ruby, Value};
5
5
  use std::fmt::{self, Debug, Formatter};
6
6
  use std::ops::Deref;
7
7
 
@@ -1,14 +1,14 @@
1
1
  use std::{ffi::c_int, os::raw::c_void, ptr::null_mut};
2
2
 
3
3
  use magnus::{
4
- ArgList, RArray, Ruby, Thread, Value,
5
4
  block::Proc,
6
- rb_sys::{AsRawId, FromRawValue, protect},
5
+ rb_sys::{protect, AsRawId, FromRawValue},
7
6
  value::{IntoId, LazyId, ReprValue},
7
+ ArgList, RArray, Ruby, Thread, Value,
8
8
  };
9
9
  use rb_sys::{
10
- VALUE, rb_funcallv, rb_thread_call_with_gvl, rb_thread_call_without_gvl, rb_thread_create,
11
- rb_thread_schedule, rb_thread_wakeup,
10
+ rb_funcallv, rb_thread_call_with_gvl, rb_thread_call_without_gvl, rb_thread_create,
11
+ rb_thread_schedule, rb_thread_wakeup, VALUE,
12
12
  };
13
13
 
14
14
  mod heap_value;
@@ -182,7 +182,11 @@ pub fn terminate_non_fork_safe_threads() {
182
182
  && !v_thread
183
183
  .funcall::<_, _, bool>(*ID_THREAD_VARIABLE_GET, (ruby.sym_new("fork_safe"),))
184
184
  .unwrap_or(false);
185
- if non_fork_safe { Some(v_thread) } else { None }
185
+ if non_fork_safe {
186
+ Some(v_thread)
187
+ } else {
188
+ None
189
+ }
186
190
  })
187
191
  .collect::<Vec<_>>();
188
192
 
@@ -10,7 +10,7 @@ publish = false
10
10
  crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
- magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
13
+ magnus = { version = "0.8.2", features = ["rb-sys", "bytes"] }
14
14
  derive_more = { version = "2.0.1", features = ["debug"] }
15
15
  itsi_tracing = { path = "../itsi_tracing" }
16
16
  itsi_rb_helpers = { path = "../itsi_rb_helpers" }
@@ -18,7 +18,7 @@ itsi_error = { path = "../itsi_error" }
18
18
  itsi_instrument_entry = { path = "../itsi_instrument_entry" }
19
19
  parking_lot = "0.12.3"
20
20
  mio = { version = "1.0.3", features = ["os-poll", "os-ext"] }
21
- rb-sys = "0.9.105"
21
+ rb-sys = "0.9.117"
22
22
  bytes = "1.10.1"
23
23
  nix = "0.29.0"
24
24
  tracing = "0.1.41"
@@ -42,7 +42,7 @@ itsi_rb_helpers = { path = "../itsi_rb_helpers" }
42
42
  itsi_tracing = { path = "../itsi_tracing" }
43
43
  itsi_acme = { path = "../itsi_acme" }
44
44
  jsonwebtoken = "9.3.1"
45
- magnus = { version = "0.7.1", features = ["bytes", "rb-sys"] }
45
+ magnus = { version = "0.8.2", features = ["bytes", "rb-sys"] }
46
46
  notify = { version = "8.0.0" }
47
47
  nix = { version = "0.29.0", features = [
48
48
  "socket",
@@ -72,7 +72,7 @@ rustls = "0.23.23"
72
72
  rustls-pemfile = "2.2.0"
73
73
  serde = "1.0.219"
74
74
  serde_json = "1.0.140"
75
- serde_magnus = "0.9.0"
75
+ serde_magnus = "0.11.0"
76
76
  sha2 = "0.10.8"
77
77
  socket2 = "0.5.8"
78
78
  sysinfo = "0.33.1"
@@ -7,7 +7,7 @@ pub mod ruby_types;
7
7
  pub mod server;
8
8
  pub mod services;
9
9
 
10
- use magnus::{error::Result, function, method, Module, Object, Ruby};
10
+ use magnus::{error::Result, function, method, Class, Module, Object, Ruby};
11
11
  use prelude::*;
12
12
  use ruby_types::{
13
13
  itsi_body_proxy::ItsiBodyProxy, itsi_grpc_call::ItsiGrpcCall,
@@ -36,7 +36,8 @@ fn init(ruby: &Ruby) -> Result<()> {
36
36
  )?;
37
37
 
38
38
  let server = ruby.get_inner(&ITSI_SERVER);
39
- server.define_singleton_method("new", function!(ItsiServer::new, 3))?;
39
+ server.define_alloc_func::<ItsiServer>();
40
+ server.define_method("initialize", method!(ItsiServer::initialize, 3))?;
40
41
  server.define_singleton_method("reset_signal_handlers", function!(reset_signal_handlers, 0))?;
41
42
  server.define_method("start", method!(ItsiServer::start, 0))?;
42
43
  server.define_method("stop", method!(ItsiServer::stop, 0))?;
@@ -13,26 +13,32 @@ use tracing::{info, instrument};
13
13
  mod file_watcher;
14
14
  pub mod itsi_server_config;
15
15
  #[magnus::wrap(class = "Itsi::Server", free_immediately, size)]
16
- #[derive(Clone)]
16
+ #[derive(Clone, Default)]
17
17
  pub struct ItsiServer {
18
- pub config: Arc<Mutex<Arc<ItsiServerConfig>>>,
18
+ pub config: Arc<Mutex<Option<Arc<ItsiServerConfig>>>>,
19
19
  }
20
20
 
21
21
  impl ItsiServer {
22
- pub fn new(
23
- ruby: &Ruby,
22
+ pub fn initialize(
23
+ &self,
24
24
  cli_params: RHash,
25
25
  itsifile_path: Option<PathBuf>,
26
26
  itsi_config_proc: Option<Proc>,
27
- ) -> Result<Self> {
28
- Ok(Self {
29
- config: Arc::new(Mutex::new(Arc::new(ItsiServerConfig::new(
30
- ruby,
31
- cli_params,
32
- itsifile_path,
33
- itsi_config_proc,
34
- )?))),
35
- })
27
+ ) -> Result<()> {
28
+ let ruby = Ruby::get().map_err(|_| {
29
+ magnus::Error::new(
30
+ magnus::exception::runtime_error(),
31
+ "Failed to acquire Ruby VM handle",
32
+ )
33
+ })?;
34
+ let config = Arc::new(ItsiServerConfig::new(
35
+ &ruby,
36
+ cli_params,
37
+ itsifile_path,
38
+ itsi_config_proc,
39
+ )?);
40
+ *self.config.lock() = Some(config);
41
+ Ok(())
36
42
  }
37
43
 
38
44
  pub fn stop(&self) -> Result<()> {
@@ -40,10 +46,20 @@ impl ItsiServer {
40
46
  Ok(())
41
47
  }
42
48
 
49
+ fn config(&self) -> Result<Arc<ItsiServerConfig>> {
50
+ self.config.lock().as_ref().cloned().ok_or_else(|| {
51
+ magnus::Error::new(
52
+ magnus::exception::runtime_error(),
53
+ "Itsi::Server not initialized",
54
+ )
55
+ })
56
+ }
57
+
43
58
  #[instrument(skip(self))]
44
59
  pub fn start(&self) -> Result<()> {
45
- self.config.lock().server_params.read().setup_listeners()?;
46
- let result = if self.config.lock().server_params.read().silence {
60
+ let server_config = self.config()?;
61
+ server_config.server_params.read().setup_listeners()?;
62
+ let result = if server_config.server_params.read().silence {
47
63
  run_silently(|| self.build_and_run_strategy())
48
64
  } else {
49
65
  info!("Itsi - Rolling into action. ⚪💨");
@@ -60,11 +76,11 @@ impl ItsiServer {
60
76
  }
61
77
 
62
78
  pub(crate) fn build_strategy(&self) -> Result<ServeStrategy> {
63
- let server_config = self.config.lock();
79
+ let server_config = self.config()?;
64
80
  Ok(if server_config.server_params.read().workers > 1 {
65
- ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config.clone())))
81
+ ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config)))
66
82
  } else {
67
- ServeStrategy::Single(Arc::new(SingleMode::new(server_config.clone(), 0)?))
83
+ ServeStrategy::Single(Arc::new(SingleMode::new(server_config, 0)?))
68
84
  })
69
85
  }
70
86
 
@@ -48,9 +48,10 @@ impl Stream for FrameStream {
48
48
  if this.shutdown_rx.has_changed().unwrap_or(false)
49
49
  && *this.shutdown_rx.borrow() == RunningPhase::ShutdownPending
50
50
  {
51
- while let Ok(bytes) = this.receiver.try_recv() {
51
+ if let Ok(bytes) = this.receiver.try_recv() {
52
52
  return Poll::Ready(Some(Ok(bytes)));
53
53
  }
54
+
54
55
  this.drained = true;
55
56
  return Poll::Ready(None);
56
57
  }
@@ -43,7 +43,7 @@ pub use error_response::ErrorResponse;
43
43
  pub use etag::ETag;
44
44
  pub use intrusion_protection::IntrusionProtection;
45
45
  pub use log_requests::LogRequests;
46
- use magnus::error::Result;
46
+ use magnus::{error::Result, Ruby};
47
47
  use magnus::rb_sys::AsRawValue;
48
48
  use magnus::Value;
49
49
  pub use max_body::MaxBody;
@@ -88,7 +88,13 @@ pub trait FromValue: Sized + Send + Sync + 'static {
88
88
  }
89
89
  }
90
90
 
91
- let deserialized: Arc<Self> = Arc::new(deserialize(value)?);
91
+ let ruby = Ruby::get().map_err(|_| {
92
+ magnus::Error::new(
93
+ magnus::exception::runtime_error(),
94
+ "Failed to acquire Ruby VM handle",
95
+ )
96
+ })?;
97
+ let deserialized: Arc<Self> = Arc::new(deserialize(&ruby, value)?);
92
98
  cache.insert(raw, deserialized.clone());
93
99
  Ok(deserialized)
94
100
  }
@@ -27,6 +27,59 @@ use std::{
27
27
  };
28
28
  use tracing::debug;
29
29
 
30
+ /// Compact representation of the client's Accept-Encoding preferences.
31
+ /// Priority order is determined by the bit checks in `pick_encoding`.
32
+ #[derive(Clone, Copy, Debug, Default)]
33
+ struct AcceptEncodingMask(u8);
34
+
35
+ impl AcceptEncodingMask {
36
+ const BR: u8 = 1 << 0;
37
+ const GZIP: u8 = 1 << 1;
38
+ const ZSTD: u8 = 1 << 2;
39
+ const DEFLATE: u8 = 1 << 3;
40
+
41
+ fn from_headers(headers: &[HeaderValue]) -> Self {
42
+ let mut mask = 0u8;
43
+
44
+ for hv in headers {
45
+ let Ok(s) = hv.to_str() else { continue };
46
+
47
+ // We intentionally ignore q-values and treat any mention as "acceptable".
48
+ // This is a fast-path optimization for common benchmark/client headers.
49
+ for part in s.split(',') {
50
+ let token = part.split(';').next().unwrap_or("").trim();
51
+ match token {
52
+ "br" => mask |= Self::BR,
53
+ "gzip" => mask |= Self::GZIP,
54
+ "zstd" => mask |= Self::ZSTD,
55
+ "deflate" => mask |= Self::DEFLATE,
56
+ _ => {}
57
+ }
58
+ }
59
+ }
60
+
61
+ Self(mask)
62
+ }
63
+
64
+ fn pick_encoding(self) -> Option<&'static str> {
65
+ // Prefer stronger/faster compression if available.
66
+ // (Actual availability is checked by the file server.)
67
+ if (self.0 & Self::ZSTD) != 0 {
68
+ return Some("zstd");
69
+ }
70
+ if (self.0 & Self::BR) != 0 {
71
+ return Some("br");
72
+ }
73
+ if (self.0 & Self::GZIP) != 0 {
74
+ return Some("gzip");
75
+ }
76
+ if (self.0 & Self::DEFLATE) != 0 {
77
+ return Some("deflate");
78
+ }
79
+ None
80
+ }
81
+ }
82
+
30
83
  #[derive(Debug, Deserialize)]
31
84
  pub struct StaticAssets {
32
85
  pub root_dir: PathBuf,
@@ -98,7 +151,10 @@ impl MiddlewareLayer for StaticAssets {
98
151
  return Ok(Either::Left(req));
99
152
  }
100
153
 
154
+ // We still populate the context cache for any other middleware that might want it,
155
+ // but we avoid re-parsing Accept-Encoding later by computing a compact mask here.
101
156
  context.set_supported_encoding_set(&req);
157
+
102
158
  let abs_path = req.uri().path();
103
159
  let rel_path = if !self.relative_path {
104
160
  abs_path.trim_start_matches("/")
@@ -119,7 +175,6 @@ impl MiddlewareLayer for StaticAssets {
119
175
  };
120
176
 
121
177
  debug!(target: "middleware::static_assets", "Asset path is {}", rel_path);
122
- // Determine if this is a HEAD request
123
178
  let is_head_request = req.method() == Method::HEAD;
124
179
 
125
180
  // Extract range and if-modified-since headers
@@ -135,6 +190,22 @@ impl MiddlewareLayer for StaticAssets {
135
190
  let encodings: &[HeaderValue] = context
136
191
  .supported_encoding_set()
137
192
  .map_or(&[], |set| set.as_slice());
193
+
194
+ // Compute a fast encoding preference and narrow the encoding list we hand to the server.
195
+ // This avoids repeated per-request string splitting/trim in the static file server.
196
+ let mask = AcceptEncodingMask::from_headers(encodings);
197
+ let preferred = mask.pick_encoding();
198
+
199
+ let narrowed: [HeaderValue; 1];
200
+ let encodings_for_server: &[HeaderValue] = if let Some(token) = preferred {
201
+ // Safe: these are valid header values and the file server only needs to see
202
+ // a minimal representation to pick a cached variant.
203
+ narrowed = [HeaderValue::from_static(token)];
204
+ &narrowed
205
+ } else {
206
+ &[]
207
+ };
208
+
138
209
  let response = file_server
139
210
  .serve(
140
211
  &req,
@@ -143,7 +214,7 @@ impl MiddlewareLayer for StaticAssets {
143
214
  serve_range,
144
215
  if_modified_since,
145
216
  is_head_request,
146
- encodings,
217
+ encodings_for_server,
147
218
  )
148
219
  .await;
149
220
 
@@ -156,40 +227,38 @@ impl MiddlewareLayer for StaticAssets {
156
227
  }
157
228
 
158
229
  fn parse_range_header(headers: &HeaderMap) -> ServeRange {
159
- let range_header = headers.get(RANGE);
160
- if range_header.is_none() {
230
+ let Some(range_header) = headers.get(RANGE) else {
161
231
  return ServeRange::Full;
162
- }
163
- let range_header = range_header.unwrap().to_str().unwrap_or("");
232
+ };
233
+
234
+ let range_header = range_header.to_str().unwrap_or("");
164
235
  let bytes_prefix = "bytes=";
165
236
  if !range_header.starts_with(bytes_prefix) {
166
237
  return ServeRange::Full;
167
238
  }
168
239
 
169
- let range_str = &range_header[bytes_prefix.len()..];
170
-
171
- let range_parts: Vec<&str> = range_str
240
+ // Only consider the first range specifier, ignore multi-range requests.
241
+ let range_str = range_header[bytes_prefix.len()..]
172
242
  .split(',')
173
243
  .next()
174
- .unwrap_or("")
175
- .split('-')
176
- .collect();
177
- if range_parts.len() != 2 {
244
+ .unwrap_or("");
245
+
246
+ let Some((start_str, end_str)) = range_str.split_once('-') else {
178
247
  return ServeRange::Full;
179
- }
248
+ };
180
249
 
181
- let start = if range_parts[0].is_empty() {
182
- range_parts[1].parse::<u64>().unwrap_or(0)
183
- } else if let Ok(start) = range_parts[0].parse::<u64>() {
250
+ let start = if start_str.is_empty() {
251
+ end_str.parse::<u64>().unwrap_or(0)
252
+ } else if let Ok(start) = start_str.parse::<u64>() {
184
253
  start
185
254
  } else {
186
255
  return ServeRange::Full;
187
256
  };
188
257
 
189
- let end = if range_parts[1].is_empty() {
190
- u64::MAX // Use u64::MAX as sentinel for open-ended ranges
191
- } else if let Ok(end) = range_parts[1].parse::<u64>() {
192
- end // No conversion needed, already u64
258
+ let end = if end_str.is_empty() {
259
+ u64::MAX // sentinel for open-ended ranges
260
+ } else if let Ok(end) = end_str.parse::<u64>() {
261
+ end
193
262
  } else {
194
263
  return ServeRange::Full;
195
264
  };
@@ -4,7 +4,7 @@ use argon2::{
4
4
  };
5
5
 
6
6
  use itsi_error::ItsiError;
7
- use magnus::{error::Result, Value};
7
+ use magnus::{error::Result, Ruby, Value};
8
8
  use serde::Deserialize;
9
9
  use serde_magnus::deserialize;
10
10
  use sha_crypt::{
@@ -26,7 +26,13 @@ pub enum HashAlgorithm {
26
26
  }
27
27
 
28
28
  pub fn create_password_hash(password: String, algo: Value) -> Result<String> {
29
- let hash_algorithm: HashAlgorithm = deserialize(algo)?;
29
+ let ruby = Ruby::get().map_err(|_| {
30
+ magnus::Error::new(
31
+ magnus::exception::runtime_error(),
32
+ "Failed to acquire Ruby VM handle",
33
+ )
34
+ })?;
35
+ let hash_algorithm: HashAlgorithm = deserialize(&ruby, algo)?;
30
36
  match hash_algorithm {
31
37
  HashAlgorithm::Bcrypt => {
32
38
  // Use the bcrypt crate for password hashing.
@@ -6,9 +6,11 @@ use redis::{Client, RedisError, Script};
6
6
  use serde::Deserialize;
7
7
  use std::any::Any;
8
8
  use std::collections::{HashMap, HashSet};
9
+ use std::fmt::Write as _;
9
10
  use std::result::Result;
11
+ use std::sync::atomic::{AtomicU64, Ordering};
10
12
  use std::sync::{Arc, LazyLock};
11
- use std::time::{Duration, Instant};
13
+ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12
14
  use tokio::sync::Mutex as AsyncMutex;
13
15
  use tokio::time::timeout;
14
16
  use tracing::warn;
@@ -303,27 +305,37 @@ impl RateLimiter for InMemoryRateLimiter {
303
305
 
304
306
  let mut entries = self.entries.write();
305
307
 
306
- let entry = entries
307
- .entry(key.to_string())
308
- .or_insert_with(|| RateLimitEntry {
309
- count: 0,
310
- expires_at: now + timeout,
311
- });
308
+ // Avoid per-request allocation: only allocate a String when inserting a new key.
309
+ //
310
+ // NOTE: we use `get_mut` first because `HashMap::entry` for `HashMap<String, _>`
311
+ // requires an owned `String`, which would allocate on every request.
312
+ if let Some(entry) = entries.get_mut(key) {
313
+ if entry.expires_at < now {
314
+ entry.expires_at = now + timeout;
315
+ entry.count = 1;
316
+ } else {
317
+ entry.count += 1;
318
+ }
319
+
320
+ let ttl = if entry.expires_at > now {
321
+ entry.expires_at.duration_since(now).as_secs()
322
+ } else {
323
+ 0
324
+ };
312
325
 
313
- if entry.expires_at < now {
314
- entry.expires_at = now + timeout;
315
- entry.count = 1;
316
- } else {
317
- entry.count += 1;
326
+ return Ok((entry.count, ttl));
318
327
  }
319
328
 
320
- let ttl = if entry.expires_at > now {
321
- entry.expires_at.duration_since(now).as_secs()
322
- } else {
323
- 0
324
- };
329
+ // Insert path: allocate once for the new key.
330
+ entries.insert(
331
+ key.to_owned(),
332
+ RateLimitEntry {
333
+ count: 1,
334
+ expires_at: now + timeout,
335
+ },
336
+ );
325
337
 
326
- Ok((entry.count, ttl))
338
+ Ok((1, timeout.as_secs()))
327
339
  }
328
340
 
329
341
  async fn check_limit(
@@ -378,14 +390,49 @@ impl BanManager {
378
390
  }
379
391
 
380
392
  /// Utility function to create a rate limit key for a specific minute
381
- pub fn create_rate_limit_key(api_key: &str, resource: &str) -> String {
382
- // Get the current minute number (0-59)
383
- let now = std::time::SystemTime::now()
384
- .duration_since(std::time::UNIX_EPOCH)
385
- .unwrap_or_default();
393
+ static CACHED_MINUTE_BUCKET_SECS: AtomicU64 = AtomicU64::new(0);
394
+ static CACHED_MINUTE_BUCKET: AtomicU64 = AtomicU64::new(0);
395
+
396
+ #[inline]
397
+ fn cached_minute_bucket() -> u64 {
398
+ // Cache the computed minute bucket and only refresh at most once per second.
399
+ // This avoids a syscall and divisions/mods on every request, while preserving
400
+ // the exact same value as the previous implementation.
401
+ let now_secs = SystemTime::now()
402
+ .duration_since(UNIX_EPOCH)
403
+ .unwrap_or_default()
404
+ .as_secs();
405
+
406
+ let last = CACHED_MINUTE_BUCKET_SECS.load(Ordering::Relaxed);
407
+ if last != now_secs {
408
+ let minutes = (now_secs / 60) % 60;
409
+ CACHED_MINUTE_BUCKET.store(minutes, Ordering::Relaxed);
410
+ CACHED_MINUTE_BUCKET_SECS.store(now_secs, Ordering::Relaxed);
411
+ minutes
412
+ } else {
413
+ CACHED_MINUTE_BUCKET.load(Ordering::Relaxed)
414
+ }
415
+ }
386
416
 
387
- let minutes = now.as_secs() / 60 % 60;
388
- format!("ratelimit:{}:{}:{}", api_key, resource, minutes)
417
+ pub fn create_rate_limit_key(api_key: &str, resource: &str) -> String {
418
+ let minutes = cached_minute_bucket();
419
+
420
+ // Build the exact same string as:
421
+ // format!("ratelimit:{}:{}:{}", api_key, resource, minutes)
422
+ // but avoid `format!` machinery and minimize reallocations.
423
+ let mut s = String::with_capacity(
424
+ "ratelimit:".len() + api_key.len() + 1 + resource.len() + 1 + 2, // minutes is 0-59
425
+ );
426
+
427
+ s.push_str("ratelimit:");
428
+ s.push_str(api_key);
429
+ s.push(':');
430
+ s.push_str(resource);
431
+ s.push(':');
432
+ // u64->decimal without intermediate allocation
433
+ let _ = write!(&mut s, "{}", minutes);
434
+
435
+ s
389
436
  }
390
437
 
391
438
  /// Utility function to create a ban key for an IP address
@@ -127,6 +127,38 @@ impl CacheEntry {
127
127
  &self,
128
128
  supported_encodings: &[HeaderValue],
129
129
  ) -> (Arc<Bytes>, Option<HeaderValue>) {
130
+ // Fast-path: if the caller already computed a preferred single encoding token,
131
+ // it will pass exactly one HeaderValue (e.g. "br"). This avoids per-request
132
+ // string splitting/parsing on the cached static file hot-path.
133
+ if supported_encodings.len() == 1 {
134
+ let hv = &supported_encodings[0];
135
+ if hv == HEADER_VALUE_ZSTD {
136
+ if let Some(zstd) = self.zstd.as_ref() {
137
+ return (zstd.clone(), Some(HEADER_VALUE_ZSTD.clone()));
138
+ }
139
+ return (self.content.clone(), None);
140
+ }
141
+ if hv == HEADER_VALUE_BR {
142
+ if let Some(br) = self.br.as_ref() {
143
+ return (br.clone(), Some(HEADER_VALUE_BR.clone()));
144
+ }
145
+ return (self.content.clone(), None);
146
+ }
147
+ if hv == HEADER_VALUE_GZIP {
148
+ if let Some(gz) = self.gz.as_ref() {
149
+ return (gz.clone(), Some(HEADER_VALUE_GZIP.clone()));
150
+ }
151
+ return (self.content.clone(), None);
152
+ }
153
+ if hv == HEADER_VALUE_DEFLATE {
154
+ if let Some(deflate) = self.deflate.as_ref() {
155
+ return (deflate.clone(), Some(HEADER_VALUE_DEFLATE.clone()));
156
+ }
157
+ return (self.content.clone(), None);
158
+ }
159
+ }
160
+
161
+ // Slow-path: parse Accept-Encoding values and select the first supported encoding.
130
162
  for encoding_header in supported_encodings {
131
163
  if let Ok(header_value) = encoding_header.to_str() {
132
164
  for header_value in header_value.split(",").map(|hv| hv.trim()) {
@@ -416,15 +448,12 @@ impl StaticFileServer {
416
448
  abs_path: &str,
417
449
  accept: ResponseFormat,
418
450
  ) -> std::result::Result<ResolvedAsset, NotFoundBehavior> {
419
- let ext_opt = Path::new(key)
420
- .extension()
421
- .and_then(|e| e.to_str())
422
- .map(|s| s.to_lowercase());
451
+ let ext_opt = Path::new(key).extension().and_then(|e| e.to_str());
423
452
 
424
453
  // If the allowed list is non-empty, enforce membership
425
454
  if !self.allowed_extensions.is_empty() {
426
455
  match ext_opt {
427
- Some(ref ext)
456
+ Some(ext)
428
457
  if self
429
458
  .allowed_extensions
430
459
  .iter()
@@ -876,10 +905,7 @@ impl StaticFileServer {
876
905
  content_length.to_string()
877
906
  },
878
907
  )
879
- .header(
880
- "Last-Modified",
881
- format_http_date_header(cache_entry.last_modified),
882
- );
908
+ .header("Last-Modified", cache_entry.last_modified_http_date.clone());
883
909
 
884
910
  if let Some(range) = content_range {
885
911
  builder = builder.header("Content-Range", range);
@@ -1,4 +1,4 @@
1
- use atty::{Stream, is};
1
+ use atty::{is, Stream};
2
2
  use std::{
3
3
  env,
4
4
  sync::{Mutex, OnceLock},
@@ -10,8 +10,8 @@ use tracing_subscriber::fmt::{
10
10
  format::{FmtSpan, JsonFields},
11
11
  writer::BoxMakeWriter,
12
12
  };
13
- use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
14
- use tracing_subscriber::{Layer, Registry, layer::Layered};
13
+ use tracing_subscriber::{fmt, prelude::*, reload, EnvFilter};
14
+ use tracing_subscriber::{layer::Layered, Layer, Registry};
15
15
 
16
16
  // Global reload handle for changing the level at runtime.
17
17
  static RELOAD_HANDLE: OnceLock<
@@ -318,9 +318,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
318
318
 
319
319
  [[package]]
320
320
  name = "magnus"
321
- version = "0.7.1"
321
+ version = "0.8.2"
322
322
  source = "registry+https://github.com/rust-lang/crates.io-index"
323
- checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab"
323
+ checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
324
324
  dependencies = [
325
325
  "bytes",
326
326
  "magnus-macros",
@@ -331,9 +331,9 @@ dependencies = [
331
331
 
332
332
  [[package]]
333
333
  name = "magnus-macros"
334
- version = "0.6.0"
334
+ version = "0.8.0"
335
335
  source = "registry+https://github.com/rust-lang/crates.io-index"
336
- checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
336
+ checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
337
337
  dependencies = [
338
338
  "proc-macro2",
339
339
  "quote",
@@ -488,18 +488,18 @@ dependencies = [
488
488
 
489
489
  [[package]]
490
490
  name = "rb-sys"
491
- version = "0.9.111"
491
+ version = "0.9.124"
492
492
  source = "registry+https://github.com/rust-lang/crates.io-index"
493
- checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af"
493
+ checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
494
494
  dependencies = [
495
495
  "rb-sys-build",
496
496
  ]
497
497
 
498
498
  [[package]]
499
499
  name = "rb-sys-build"
500
- version = "0.9.111"
500
+ version = "0.9.124"
501
501
  source = "registry+https://github.com/rust-lang/crates.io-index"
502
- checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29"
502
+ checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
503
503
  dependencies = [
504
504
  "bindgen",
505
505
  "lazy_static",
@@ -512,9 +512,9 @@ dependencies = [
512
512
 
513
513
  [[package]]
514
514
  name = "rb-sys-env"
515
- version = "0.1.2"
515
+ version = "0.2.3"
516
516
  source = "registry+https://github.com/rust-lang/crates.io-index"
517
- checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
517
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
518
518
 
519
519
  [[package]]
520
520
  name = "rcgen"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.2.21.rc1"
5
+ VERSION = "0.2.21.rc2"
6
6
  end
7
7
  end
@@ -1906,9 +1906,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
1906
1906
 
1907
1907
  [[package]]
1908
1908
  name = "magnus"
1909
- version = "0.7.1"
1909
+ version = "0.8.2"
1910
1910
  source = "registry+https://github.com/rust-lang/crates.io-index"
1911
- checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab"
1911
+ checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
1912
1912
  dependencies = [
1913
1913
  "bytes",
1914
1914
  "magnus-macros",
@@ -1919,9 +1919,9 @@ dependencies = [
1919
1919
 
1920
1920
  [[package]]
1921
1921
  name = "magnus-macros"
1922
- version = "0.6.0"
1922
+ version = "0.8.0"
1923
1923
  source = "registry+https://github.com/rust-lang/crates.io-index"
1924
- checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
1924
+ checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
1925
1925
  dependencies = [
1926
1926
  "proc-macro2",
1927
1927
  "quote",
@@ -2554,18 +2554,18 @@ dependencies = [
2554
2554
 
2555
2555
  [[package]]
2556
2556
  name = "rb-sys"
2557
- version = "0.9.111"
2557
+ version = "0.9.124"
2558
2558
  source = "registry+https://github.com/rust-lang/crates.io-index"
2559
- checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af"
2559
+ checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
2560
2560
  dependencies = [
2561
2561
  "rb-sys-build",
2562
2562
  ]
2563
2563
 
2564
2564
  [[package]]
2565
2565
  name = "rb-sys-build"
2566
- version = "0.9.111"
2566
+ version = "0.9.124"
2567
2567
  source = "registry+https://github.com/rust-lang/crates.io-index"
2568
- checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29"
2568
+ checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
2569
2569
  dependencies = [
2570
2570
  "bindgen",
2571
2571
  "lazy_static",
@@ -2578,9 +2578,9 @@ dependencies = [
2578
2578
 
2579
2579
  [[package]]
2580
2580
  name = "rb-sys-env"
2581
- version = "0.1.2"
2581
+ version = "0.2.3"
2582
2582
  source = "registry+https://github.com/rust-lang/crates.io-index"
2583
- checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
2583
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
2584
2584
 
2585
2585
  [[package]]
2586
2586
  name = "rcgen"
@@ -2972,9 +2972,9 @@ dependencies = [
2972
2972
 
2973
2973
  [[package]]
2974
2974
  name = "serde_magnus"
2975
- version = "0.9.0"
2975
+ version = "0.11.0"
2976
2976
  source = "registry+https://github.com/rust-lang/crates.io-index"
2977
- checksum = "51b8b945a2dadb221f1c5490cfb411cab6c3821446b8eca50ee07e5a3893ec51"
2977
+ checksum = "8ff64c88ddd26acdcad5a501f18bcc339927b77b69f4a03bfaf2a6fc5ba2ac4b"
2978
2978
  dependencies = [
2979
2979
  "magnus",
2980
2980
  "serde",
@@ -27,6 +27,21 @@ module Itsi
27
27
 
28
28
  RACK_HEADER_MAP.default_proc = proc { |_, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
29
29
 
30
+ SPECIAL_RACK_HEADER_MAP = {
31
+ "content-type" => "CONTENT_TYPE",
32
+ "content-length" => "CONTENT_LENGTH",
33
+ "accept" => "HTTP_ACCEPT",
34
+ "accept-encoding" => "HTTP_ACCEPT_ENCODING",
35
+ "accept-language" => "HTTP_ACCEPT_LANGUAGE",
36
+ "user-agent" => "HTTP_USER_AGENT",
37
+ "referer" => "HTTP_REFERER",
38
+ "origin" => "HTTP_ORIGIN",
39
+ "cookie" => "HTTP_COOKIE",
40
+ "authorization" => "HTTP_AUTHORIZATION",
41
+ "x-forwarded-for" => "HTTP_X_FORWARDED_FOR",
42
+ "x-forwarded-proto" => "HTTP_X_FORWARDED_PROTO"
43
+ }.freeze
44
+
30
45
  HTTP_09 = "HTTP/0.9"
31
46
  HTTP_09_ARR = ["HTTP/0.9"].freeze
32
47
  HTTP_10 = "HTTP/1.0"
@@ -63,24 +78,9 @@ module Itsi
63
78
  end
64
79
  env["rack.url_scheme"] = scheme
65
80
  env["rack.input"] = build_input_io
66
- env["rack.hijack"] = method(:hijack)
81
+ env["rack.hijack"] = self
67
82
  each_header do |k, v|
68
- env[case k
69
- when "content-type" then "CONTENT_TYPE"
70
- when "content-length" then "CONTENT_LENGTH"
71
- when "accept" then "HTTP_ACCEPT"
72
- when "accept-encoding" then "HTTP_ACCEPT_ENCODING"
73
- when "accept-language" then "HTTP_ACCEPT_LANGUAGE"
74
- when "user-agent" then "HTTP_USER_AGENT"
75
- when "referer" then "HTTP_REFERER"
76
- when "origin" then "HTTP_ORIGIN"
77
- when "cookie" then "HTTP_COOKIE"
78
- when "authorization" then "HTTP_AUTHORIZATION"
79
- when "x-forwarded-for" then "HTTP_X_FORWARDED_FOR"
80
- when "x-forwarded-proto" then "HTTP_X_FORWARDED_PROTO"
81
- else RACK_HEADER_MAP[k]
82
- end
83
- ] = v
83
+ env[SPECIAL_RACK_HEADER_MAP[k] || RACK_HEADER_MAP[k]] = v
84
84
  end
85
85
  env
86
86
  end
@@ -138,6 +138,11 @@ module Itsi
138
138
  end
139
139
  end
140
140
 
141
+ # Rack expects env["rack.hijack"] to respond to #call.
142
+ def call
143
+ hijack
144
+ end
145
+
141
146
  def body
142
147
  @body ||= build_input_io
143
148
  end
@@ -32,23 +32,13 @@ module Itsi
32
32
 
33
33
  POOL = []
34
34
 
35
- def self.checkout # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
36
- POOL.pop&.tap do |recycled|
37
- recycled.keys.each do |key|
38
- case key
39
- when "SERVER_SOFTWARE" then recycled[key] = "Itsi"
40
- when "rack.errors" then recycled[key] = $stderr
41
- when "rack.multithread", "rack.multiprocess", "rack.hijack?" then recycled[key] = true
42
- when "rack.run_once" then recycled[key] = false
43
- when "rack.multipart.buffer_size" then recycled[key] = 16_384
44
- when "SCRIPT_NAME", "REQUEST_METHOD", "PATH_INFO", "REQUEST_PATH", "QUERY_STRING", "REMOTE_ADDR",
45
- "SERVER_PORT", "SERVER_NAME", "SERVER_PROTOCOL", "HTTP_HOST", "HTTP_VERSION", "itsi.request",
46
- "itsi.response", "rack.version", "rack.url_scheme", "rack.input", "rack.hijack"
47
- nil
48
- else recycled.delete(key)
49
- end
50
- end
51
- end || RACK_ENV_TEMPLATE.dup
35
+ def self.checkout
36
+ recycled = POOL.pop
37
+ return RACK_ENV_TEMPLATE.dup unless recycled
38
+
39
+ # Reset in C rather than iterating key-by-key in Ruby for every request.
40
+ recycled.replace(RACK_ENV_TEMPLATE)
41
+ recycled
52
42
  end
53
43
 
54
44
  def self.checkin(env)
@@ -33,9 +33,16 @@ module Itsi
33
33
  }
34
34
  end
35
35
 
36
+ RedirectTarget = TypedStruct.new do
37
+ {
38
+ to: (Required() & Type(String)),
39
+ type: Enum(["permanent", "temporary", "found", "moved_permanently"]).default("moved_permanently")
40
+ }
41
+ end
42
+
36
43
  RedirectResponse = TypedStruct.new do
37
44
  {
38
- redirect: Type(Redirect::Redirect) & Required()
45
+ redirect: Type(RedirectTarget) & Required()
39
46
  }
40
47
  end
41
48
 
@@ -62,6 +62,18 @@ module Itsi
62
62
  # stream this response.
63
63
  body_streamer.call(response)
64
64
 
65
+ elsif body.is_a?(Array)
66
+ if body.length == 1
67
+ response.send_and_close(body[0].to_s)
68
+ else
69
+ buffer = nil
70
+ body.each do |part|
71
+ response << buffer.to_s if buffer
72
+ buffer = part
73
+ end
74
+
75
+ response.send_and_close(buffer.to_s)
76
+ end
65
77
  elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
66
78
  # If we're enumerable with more than one chunk
67
79
  # also stream, otherwise write in a single chunk
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.21.rc1"
5
+ VERSION = "0.2.21.rc2"
6
6
  end
7
7
  end
data/lib/itsi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Itsi
2
- VERSION = "0.2.21.rc1"
2
+ VERSION = "0.2.21.rc2"
3
3
  end
data/mise.toml ADDED
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "4.0.1"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.21.rc1
4
+ version: 0.2.21.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.21.rc1
18
+ version: 0.2.21.rc2
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.2.21.rc1
25
+ version: 0.2.21.rc2
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: itsi-server
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.2.21.rc1
32
+ version: 0.2.21.rc2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.2.21.rc1
39
+ version: 0.2.21.rc2
40
40
  description: Wrapper Gem for both the Itsi server and the Itsi Fiber scheduler
41
41
  email:
42
42
  - wc@pico.net.nz
@@ -44,10 +44,12 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
+ - ".dockerignore"
47
48
  - ".zed/settings.json"
48
49
  - CHANGELOG.md
49
50
  - Cargo.lock
50
51
  - Cargo.toml
52
+ - Dockerfile
51
53
  - LICENSE.txt
52
54
  - README.md
53
55
  - Rakefile
@@ -1119,6 +1121,7 @@ files:
1119
1121
  - itsi-server-100.png
1120
1122
  - lib/itsi.rb
1121
1123
  - lib/itsi/version.rb
1124
+ - mise.toml
1122
1125
  homepage: https://itsi.fyi
1123
1126
  licenses:
1124
1127
  - LGPL-3.0