@nmtjs/proxy 0.15.0-beta.8 → 1.0.0-alpha.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.
- package/package.json +25 -10
- package/Cargo.lock +0 -2681
- package/Cargo.toml +0 -26
- package/LICENSE.md +0 -7
- package/README.md +0 -9
- package/build.rs +0 -3
- package/src/config.rs +0 -199
- package/src/lib.rs +0 -8
- package/src/proxy.rs +0 -147
- package/src/router.rs +0 -406
package/Cargo.toml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "neemata-proxy"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
edition = "2024"
|
|
5
|
-
|
|
6
|
-
[lib]
|
|
7
|
-
crate-type = ["cdylib"]
|
|
8
|
-
|
|
9
|
-
[dependencies]
|
|
10
|
-
napi = { version = "3", features = ["async"] }
|
|
11
|
-
napi-derive = "3.0.0"
|
|
12
|
-
async-trait = "0.1"
|
|
13
|
-
pingora = { version = "0.6", features = ["lb"] }
|
|
14
|
-
pingora-load-balancing = "0.6"
|
|
15
|
-
http = "1.0"
|
|
16
|
-
tokio = { version = "1.48.0", features = ["sync"] }
|
|
17
|
-
url = "2.5.7"
|
|
18
|
-
log = "0.4.28"
|
|
19
|
-
env_logger = "0.11.8"
|
|
20
|
-
|
|
21
|
-
[build-dependencies]
|
|
22
|
-
napi-build = "2"
|
|
23
|
-
|
|
24
|
-
[profile.release]
|
|
25
|
-
lto = true
|
|
26
|
-
strip = "symbols"
|
package/LICENSE.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2025 Denys Ilchyshyn
|
|
2
|
-
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
-
|
|
5
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
-
|
|
7
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# NeemataJS - RPC application server for real-time applications (proof of concept)
|
|
2
|
-
|
|
3
|
-
### Built with following in mind:
|
|
4
|
-
- transport-agnostic (like WebSockets, WebTransport, .etc)
|
|
5
|
-
- format-agnostic (like JSON, MessagePack, BSON, .etc)
|
|
6
|
-
- binary data streaming and event subscriptions
|
|
7
|
-
- contract-based API
|
|
8
|
-
- end-to-end type safety
|
|
9
|
-
- CPU-intensive task execution on separate workers
|
package/build.rs
DELETED
package/src/config.rs
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
use napi::bindgen_prelude::*;
|
|
2
|
-
use napi_derive::napi;
|
|
3
|
-
use std::{collections::HashMap, time::Duration};
|
|
4
|
-
use url::Url;
|
|
5
|
-
|
|
6
|
-
#[napi(object)]
|
|
7
|
-
pub struct ApplicationUpstreamConfig {
|
|
8
|
-
pub url: String,
|
|
9
|
-
#[napi(ts_type = "'http' | 'websocket'")]
|
|
10
|
-
pub r#type: String,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
#[napi(object)]
|
|
14
|
-
pub struct ApplicationConfig {
|
|
15
|
-
pub upstreams: Vec<ApplicationUpstreamConfig>,
|
|
16
|
-
pub sni: Option<String>,
|
|
17
|
-
pub enable_tls: Option<bool>,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
#[napi(object)]
|
|
21
|
-
pub struct ProxyOptions {
|
|
22
|
-
pub listen: Option<String>,
|
|
23
|
-
pub tls: Option<bool>,
|
|
24
|
-
pub threads: Option<u16>,
|
|
25
|
-
pub health_check_interval: Option<u32>,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
|
29
|
-
pub enum UpstreamKind {
|
|
30
|
-
Http,
|
|
31
|
-
Websocket,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
impl UpstreamKind {
|
|
35
|
-
pub fn as_str(&self) -> &'static str {
|
|
36
|
-
match self {
|
|
37
|
-
Self::Http => "http",
|
|
38
|
-
Self::Websocket => "websocket",
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
#[derive(Debug, Clone)]
|
|
44
|
-
pub enum AppUpstream {
|
|
45
|
-
Port {
|
|
46
|
-
secure: bool,
|
|
47
|
-
#[allow(dead_code)]
|
|
48
|
-
address: String,
|
|
49
|
-
port: u16,
|
|
50
|
-
hostname: String,
|
|
51
|
-
},
|
|
52
|
-
Unix {
|
|
53
|
-
secure: bool,
|
|
54
|
-
path: String,
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
pub struct AppDefinition {
|
|
59
|
-
pub upstreams: HashMap<UpstreamKind, Vec<AppUpstream>>,
|
|
60
|
-
pub sni: Option<String>,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
pub struct ProxyConfig {
|
|
64
|
-
pub apps: HashMap<String, AppDefinition>,
|
|
65
|
-
pub listen: Option<String>,
|
|
66
|
-
pub threads: Option<u16>,
|
|
67
|
-
pub tls: bool,
|
|
68
|
-
pub health_check_interval: Option<Duration>,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
impl ProxyConfig {
|
|
72
|
-
pub fn from_inputs(
|
|
73
|
-
apps: HashMap<String, ApplicationConfig>,
|
|
74
|
-
options: Option<ProxyOptions>,
|
|
75
|
-
) -> Result<Self> {
|
|
76
|
-
if apps.is_empty() {
|
|
77
|
-
return Err(Error::from_reason(
|
|
78
|
-
"apps map must contain at least one application",
|
|
79
|
-
));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let mut normalized_apps = HashMap::with_capacity(apps.len());
|
|
83
|
-
for (name, config) in apps {
|
|
84
|
-
if config.upstreams.is_empty() {
|
|
85
|
-
return Err(Error::from_reason(format!(
|
|
86
|
-
"application '{name}' must provide at least one upstream"
|
|
87
|
-
)));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let mut upstreams = HashMap::new();
|
|
91
|
-
|
|
92
|
-
for upstream in config.upstreams {
|
|
93
|
-
let trimmed = upstream.url.trim().to_string();
|
|
94
|
-
if trimmed.is_empty() {
|
|
95
|
-
return Err(Error::from_reason(format!(
|
|
96
|
-
"upstream URL cannot be empty in application '{name}'"
|
|
97
|
-
)));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let parsed = Url::parse(&trimmed).map_err(|_| {
|
|
101
|
-
Error::from_reason(format!(
|
|
102
|
-
"invalid URL '{trimmed}' in upstreams for application '{name}'"
|
|
103
|
-
))
|
|
104
|
-
})?;
|
|
105
|
-
|
|
106
|
-
let secure = match parsed.scheme() {
|
|
107
|
-
"https" | "wss" | "https+unix" | "wss+unix" => true,
|
|
108
|
-
"http" | "ws" | "http+unix" | "ws+unix" => false,
|
|
109
|
-
other => {
|
|
110
|
-
return Err(Error::from_reason(format!(
|
|
111
|
-
"unsupported URL scheme '{other}' in upstream '{trimmed}'"
|
|
112
|
-
)));
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
let upstream_type = match upstream.r#type.as_str() {
|
|
117
|
-
"http" => UpstreamKind::Http,
|
|
118
|
-
"websocket" => UpstreamKind::Websocket,
|
|
119
|
-
other => {
|
|
120
|
-
return Err(Error::from_reason(format!(
|
|
121
|
-
"unsupported upstream type '{other}' for application '{name}'"
|
|
122
|
-
)));
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
let upstream = if parsed.scheme().ends_with("+unix") {
|
|
127
|
-
AppUpstream::Unix {
|
|
128
|
-
secure,
|
|
129
|
-
path: parsed.path().to_string(),
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
let host = parsed.host_str().ok_or_else(|| {
|
|
133
|
-
Error::from_reason(format!(
|
|
134
|
-
"missing host in upstream URL '{trimmed}' for application '{name}'"
|
|
135
|
-
))
|
|
136
|
-
})?;
|
|
137
|
-
let port = parsed.port_or_known_default().ok_or_else(|| {
|
|
138
|
-
Error::from_reason(format!(
|
|
139
|
-
"missing port in upstream URL '{trimmed}' for application '{name}'"
|
|
140
|
-
))
|
|
141
|
-
})?;
|
|
142
|
-
AppUpstream::Port {
|
|
143
|
-
secure,
|
|
144
|
-
address: parsed.to_string(),
|
|
145
|
-
port,
|
|
146
|
-
hostname: host.to_string(),
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
upstreams
|
|
151
|
-
.entry(upstream_type)
|
|
152
|
-
.or_insert_with(Vec::new)
|
|
153
|
-
.push(upstream);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if upstreams.is_empty() {
|
|
157
|
-
return Err(Error::from_reason(format!(
|
|
158
|
-
"application '{name}' produced no valid upstreams"
|
|
159
|
-
)));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let definition = AppDefinition {
|
|
163
|
-
upstreams,
|
|
164
|
-
sni: config.sni.filter(|value| !value.is_empty()),
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
normalized_apps.insert(name, definition);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
let options = options.unwrap_or_default();
|
|
171
|
-
|
|
172
|
-
let health_check_interval = options
|
|
173
|
-
.health_check_interval
|
|
174
|
-
.map(|secs| Duration::from_secs(secs as u64));
|
|
175
|
-
|
|
176
|
-
Ok(Self {
|
|
177
|
-
apps: normalized_apps,
|
|
178
|
-
listen: options.listen,
|
|
179
|
-
threads: options.threads,
|
|
180
|
-
tls: options.tls.unwrap_or(false),
|
|
181
|
-
health_check_interval,
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
pub fn listener(&self) -> &str {
|
|
186
|
-
self.listen.as_deref().unwrap_or("0.0.0.0:6188")
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
impl Default for ProxyOptions {
|
|
191
|
-
fn default() -> Self {
|
|
192
|
-
Self {
|
|
193
|
-
listen: None,
|
|
194
|
-
threads: None,
|
|
195
|
-
tls: None,
|
|
196
|
-
health_check_interval: Some(30),
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
package/src/lib.rs
DELETED
package/src/proxy.rs
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
use crate::{
|
|
2
|
-
config::{ApplicationConfig, ProxyConfig, ProxyOptions},
|
|
3
|
-
router::{RouterAssembly, build_router},
|
|
4
|
-
};
|
|
5
|
-
use log::{debug, info};
|
|
6
|
-
use napi::Error as NapiError;
|
|
7
|
-
use napi_derive::napi;
|
|
8
|
-
use pingora::{
|
|
9
|
-
prelude::{Opt, Server, http_proxy_service},
|
|
10
|
-
server::{RunArgs, ShutdownSignal, ShutdownSignalWatch, configuration::ServerConf},
|
|
11
|
-
};
|
|
12
|
-
use std::{collections::HashMap, sync::Mutex, thread};
|
|
13
|
-
use tokio::sync::oneshot;
|
|
14
|
-
|
|
15
|
-
#[napi]
|
|
16
|
-
pub struct NeemataProxy {
|
|
17
|
-
server: Option<Server>,
|
|
18
|
-
shutdown_tx: Option<oneshot::Sender<ShutdownSignal>>,
|
|
19
|
-
runner: Option<thread::JoinHandle<()>>,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
#[napi]
|
|
23
|
-
impl NeemataProxy {
|
|
24
|
-
#[napi(constructor)]
|
|
25
|
-
pub fn new(
|
|
26
|
-
apps: HashMap<String, ApplicationConfig>,
|
|
27
|
-
options: Option<ProxyOptions>,
|
|
28
|
-
) -> napi::Result<Self> {
|
|
29
|
-
env_logger::try_init().ok();
|
|
30
|
-
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS: u64 = 1;
|
|
31
|
-
let config = ProxyConfig::from_inputs(apps, options)?;
|
|
32
|
-
|
|
33
|
-
let mut server_conf = ServerConf::default();
|
|
34
|
-
let server_opts = Opt {
|
|
35
|
-
daemon: false,
|
|
36
|
-
..Default::default()
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
server_conf.grace_period_seconds = Some(DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS - 1);
|
|
40
|
-
server_conf.graceful_shutdown_timeout_seconds =
|
|
41
|
-
Some(DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS);
|
|
42
|
-
server_conf.threads = config.threads.unwrap_or(1) as usize;
|
|
43
|
-
server_conf.work_stealing = true;
|
|
44
|
-
|
|
45
|
-
let mut server = Server::new_with_opt_and_conf(server_opts, server_conf);
|
|
46
|
-
|
|
47
|
-
let RouterAssembly {
|
|
48
|
-
router,
|
|
49
|
-
background_services,
|
|
50
|
-
} = build_router(&config)?;
|
|
51
|
-
|
|
52
|
-
let mut proxy_service = http_proxy_service(&server.configuration, router);
|
|
53
|
-
|
|
54
|
-
if config.tls {
|
|
55
|
-
// TODO: support TLS options
|
|
56
|
-
panic!("TLS is not supported yet");
|
|
57
|
-
} else {
|
|
58
|
-
proxy_service.add_tcp(config.listener());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
server.add_service(proxy_service);
|
|
62
|
-
|
|
63
|
-
for service in background_services {
|
|
64
|
-
server.add_service(service);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
Ok(Self {
|
|
68
|
-
server: Some(server),
|
|
69
|
-
shutdown_tx: None,
|
|
70
|
-
runner: None,
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
#[napi]
|
|
75
|
-
pub fn run(&mut self) -> napi::Result<()> {
|
|
76
|
-
if self.runner.is_some() {
|
|
77
|
-
return Err(NapiError::from_reason("server already running"));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let server = self
|
|
81
|
-
.server
|
|
82
|
-
.take()
|
|
83
|
-
.ok_or_else(|| NapiError::from_reason("server already running"))?;
|
|
84
|
-
|
|
85
|
-
let (tx, rx) = oneshot::channel();
|
|
86
|
-
self.shutdown_tx = Some(tx);
|
|
87
|
-
|
|
88
|
-
let handle = thread::spawn(move || {
|
|
89
|
-
let args = RunArgs {
|
|
90
|
-
shutdown_signal: Box::new(JsShutdownWatch::new(rx)),
|
|
91
|
-
};
|
|
92
|
-
server.run(args);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
self.runner = Some(handle);
|
|
96
|
-
Ok(())
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
#[napi]
|
|
100
|
-
pub fn shutdown(&mut self) -> napi::Result<()> {
|
|
101
|
-
info!("Shutting down server. Sending shutdown signal...");
|
|
102
|
-
let tx = self.shutdown_tx.take().unwrap();
|
|
103
|
-
let _ = tx.send(ShutdownSignal::GracefulTerminate);
|
|
104
|
-
|
|
105
|
-
info!("Joining server thread...");
|
|
106
|
-
let handle = self.runner.take().unwrap();
|
|
107
|
-
let _ = handle.join();
|
|
108
|
-
info!("Server shut down successfully.");
|
|
109
|
-
Ok(())
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
struct JsShutdownWatch {
|
|
114
|
-
receiver: Mutex<Option<oneshot::Receiver<ShutdownSignal>>>,
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
impl JsShutdownWatch {
|
|
118
|
-
fn new(receiver: oneshot::Receiver<ShutdownSignal>) -> Self {
|
|
119
|
-
Self {
|
|
120
|
-
receiver: Mutex::new(Some(receiver)),
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
#[async_trait::async_trait]
|
|
126
|
-
impl ShutdownSignalWatch for JsShutdownWatch {
|
|
127
|
-
async fn recv(&self) -> ShutdownSignal {
|
|
128
|
-
let receiver = {
|
|
129
|
-
let mut guard = match self.receiver.lock() {
|
|
130
|
-
Ok(guard) => guard,
|
|
131
|
-
Err(poisoned) => poisoned.into_inner(),
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
guard.take()
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
match receiver {
|
|
138
|
-
Some(rx) => {
|
|
139
|
-
debug!("Waiting for shutdown signal from JS...");
|
|
140
|
-
let signal = rx.await.unwrap_or(ShutdownSignal::FastShutdown);
|
|
141
|
-
info!("Received shutdown signal from JS: {:?}", signal);
|
|
142
|
-
signal
|
|
143
|
-
}
|
|
144
|
-
None => ShutdownSignal::FastShutdown,
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|