@celerity-sdk/runtime 0.2.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/.env.test +4 -0
- package/.vscode/settings.json +4 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +121 -0
- package/Cargo.toml +38 -0
- package/README.md +7 -0
- package/build.rs +3 -0
- package/index.d.ts +96 -0
- package/index.js +581 -0
- package/package.json +67 -0
- package/src/lib.rs +561 -0
- package/tsconfig.json +12 -0
package/src/lib.rs
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#![deny(clippy::all)]
|
|
2
|
+
#![allow(unexpected_cfgs)]
|
|
3
|
+
|
|
4
|
+
use std::{collections::HashMap, sync::Arc, time::Duration};
|
|
5
|
+
|
|
6
|
+
use axum::{
|
|
7
|
+
body::Body,
|
|
8
|
+
http::{request::Parts, Request, StatusCode},
|
|
9
|
+
response::IntoResponse,
|
|
10
|
+
};
|
|
11
|
+
use celerity_helpers::{
|
|
12
|
+
env::ProcessEnvVars,
|
|
13
|
+
request::{
|
|
14
|
+
cookies_from_headers, path_params_from_request_parts, query_from_uri, to_request_body,
|
|
15
|
+
},
|
|
16
|
+
runtime_types::{RuntimeCallMode, RuntimePlatform},
|
|
17
|
+
};
|
|
18
|
+
use celerity_runtime_core::{
|
|
19
|
+
application::Application,
|
|
20
|
+
auth_http::AuthClaims,
|
|
21
|
+
config::{
|
|
22
|
+
ApiConfig, AppConfig, ClientIpSource, HttpConfig, HttpHandlerDefinition, RuntimeConfig,
|
|
23
|
+
WebSocketConfig,
|
|
24
|
+
},
|
|
25
|
+
request::{MatchedRoute, RequestId, ResolvedClientIp, ResolvedUserAgent},
|
|
26
|
+
telemetry_utils::extract_trace_context,
|
|
27
|
+
};
|
|
28
|
+
use napi::bindgen_prelude::*;
|
|
29
|
+
use napi::threadsafe_function::ThreadsafeFunction;
|
|
30
|
+
use napi_derive::napi;
|
|
31
|
+
use serde::{Deserialize, Serialize};
|
|
32
|
+
use tokio::time;
|
|
33
|
+
|
|
34
|
+
const MAX_REQUEST_BODY_SIZE: usize = 10 * 1024 * 1024; // 10 MiB
|
|
35
|
+
|
|
36
|
+
/// A weak ThreadsafeFunction that does not prevent the Node.js event loop from exiting.
|
|
37
|
+
type WeakTsfn =
|
|
38
|
+
ThreadsafeFunction<JsRequestWrapper, Promise<Response>, JsRequestWrapper, Status, true, true>;
|
|
39
|
+
|
|
40
|
+
#[napi(object)]
|
|
41
|
+
pub struct CoreRuntimeConfig {
|
|
42
|
+
pub blueprint_config_path: String,
|
|
43
|
+
pub server_port: i32,
|
|
44
|
+
pub server_loopback_only: Option<bool>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[napi(object)]
|
|
48
|
+
pub struct CoreRuntimeAppConfig {
|
|
49
|
+
pub api: Option<CoreApiConfig>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl From<AppConfig> for CoreRuntimeAppConfig {
|
|
53
|
+
fn from(app_config: AppConfig) -> Self {
|
|
54
|
+
let api = app_config.api.map(|api_config| api_config.into());
|
|
55
|
+
Self { api }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[napi(object)]
|
|
60
|
+
pub struct CoreApiConfig {
|
|
61
|
+
pub http: Option<CoreHttpConfig>,
|
|
62
|
+
pub websocket: Option<CoreWebsocketConfig>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl From<ApiConfig> for CoreApiConfig {
|
|
66
|
+
fn from(api_config: ApiConfig) -> Self {
|
|
67
|
+
let http = api_config.http.map(|http_config| http_config.into());
|
|
68
|
+
let websocket = api_config
|
|
69
|
+
.websocket
|
|
70
|
+
.map(|websocket_config| websocket_config.into());
|
|
71
|
+
Self { http, websocket }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[napi(object)]
|
|
76
|
+
pub struct CoreHttpConfig {
|
|
77
|
+
pub handlers: Vec<CoreHttpHandlerDefinition>,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl From<HttpConfig> for CoreHttpConfig {
|
|
81
|
+
fn from(http_config: HttpConfig) -> Self {
|
|
82
|
+
let handlers = http_config
|
|
83
|
+
.handlers
|
|
84
|
+
.into_iter()
|
|
85
|
+
.map(|handler| handler.into())
|
|
86
|
+
.collect::<Vec<_>>();
|
|
87
|
+
Self { handlers }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[napi(object)]
|
|
92
|
+
pub struct CoreWebsocketConfig {}
|
|
93
|
+
|
|
94
|
+
impl From<WebSocketConfig> for CoreWebsocketConfig {
|
|
95
|
+
fn from(_: WebSocketConfig) -> Self {
|
|
96
|
+
Self {}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[napi(object)]
|
|
101
|
+
pub struct CoreHttpHandlerDefinition {
|
|
102
|
+
pub path: String,
|
|
103
|
+
pub method: String,
|
|
104
|
+
pub location: String,
|
|
105
|
+
pub handler: String,
|
|
106
|
+
pub timeout: i64,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl From<HttpHandlerDefinition> for CoreHttpHandlerDefinition {
|
|
110
|
+
fn from(handler: HttpHandlerDefinition) -> Self {
|
|
111
|
+
Self {
|
|
112
|
+
path: handler.path,
|
|
113
|
+
method: handler.method,
|
|
114
|
+
location: handler.location,
|
|
115
|
+
handler: handler.handler,
|
|
116
|
+
timeout: handler.timeout,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[napi(object)]
|
|
122
|
+
pub struct Response {
|
|
123
|
+
pub status: u16,
|
|
124
|
+
pub headers: Option<HashMap<String, String>>,
|
|
125
|
+
pub body: Option<String>,
|
|
126
|
+
pub binary_body: Option<Buffer>,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl IntoResponse for Response {
|
|
130
|
+
fn into_response(self) -> axum::response::Response<Body> {
|
|
131
|
+
let mut builder = axum::response::Response::builder();
|
|
132
|
+
for (key, value) in self.headers.unwrap_or_default() {
|
|
133
|
+
builder = builder.header(key, value);
|
|
134
|
+
}
|
|
135
|
+
builder = builder.status(self.status);
|
|
136
|
+
let body = if let Some(binary) = self.binary_body {
|
|
137
|
+
Body::from(binary.to_vec())
|
|
138
|
+
} else {
|
|
139
|
+
Body::from(self.body.unwrap_or_default())
|
|
140
|
+
};
|
|
141
|
+
builder.body(body).unwrap()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[derive(Debug)]
|
|
146
|
+
pub enum JsRequestWrapperBody {
|
|
147
|
+
Text(String),
|
|
148
|
+
Binary(Vec<u8>),
|
|
149
|
+
EmptyBody,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[napi(js_name = "Request")]
|
|
153
|
+
pub struct JsRequestWrapper {
|
|
154
|
+
inner_body: JsRequestWrapperBody,
|
|
155
|
+
inner_parts: Parts,
|
|
156
|
+
path_params: HashMap<String, String>,
|
|
157
|
+
query: HashMap<String, Vec<String>>,
|
|
158
|
+
cookies: HashMap<String, String>,
|
|
159
|
+
content_type: String,
|
|
160
|
+
req_path: String,
|
|
161
|
+
request_id: String,
|
|
162
|
+
request_time: String,
|
|
163
|
+
auth_claims: Option<serde_json::Value>,
|
|
164
|
+
client_ip: String,
|
|
165
|
+
trace_context: Option<HashMap<String, String>>,
|
|
166
|
+
user_agent: String,
|
|
167
|
+
matched_route: Option<String>,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[napi]
|
|
171
|
+
impl JsRequestWrapper {
|
|
172
|
+
/// Allows the creation of requests, primarily for test purposes.
|
|
173
|
+
/// In normal circumstances, the request will be created by
|
|
174
|
+
/// the runtime and passed to the handler.
|
|
175
|
+
#[napi(constructor)]
|
|
176
|
+
pub fn new(method: String, uri: String, headers: HashMap<String, String>) -> Self {
|
|
177
|
+
let mut builder = Request::builder().method(method.as_str()).uri(uri.clone());
|
|
178
|
+
for (key, value) in headers {
|
|
179
|
+
builder = builder.header(key, value);
|
|
180
|
+
}
|
|
181
|
+
let request = builder.body(Body::empty()).unwrap();
|
|
182
|
+
let (parts, _) = request.into_parts();
|
|
183
|
+
Self {
|
|
184
|
+
inner_parts: parts,
|
|
185
|
+
inner_body: JsRequestWrapperBody::EmptyBody,
|
|
186
|
+
path_params: HashMap::new(),
|
|
187
|
+
query: HashMap::new(),
|
|
188
|
+
cookies: HashMap::new(),
|
|
189
|
+
content_type: String::new(),
|
|
190
|
+
req_path: uri,
|
|
191
|
+
request_id: String::new(),
|
|
192
|
+
request_time: String::new(),
|
|
193
|
+
auth_claims: None,
|
|
194
|
+
client_ip: String::new(),
|
|
195
|
+
trace_context: None,
|
|
196
|
+
user_agent: String::new(),
|
|
197
|
+
matched_route: None,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async fn from_axum_req(req: axum::extract::Request<Body>) -> Result<Self> {
|
|
202
|
+
let (mut parts, body) = req.into_parts();
|
|
203
|
+
|
|
204
|
+
// Extract pre-processed fields before consuming the body.
|
|
205
|
+
let path_params = path_params_from_request_parts(&mut parts)
|
|
206
|
+
.await
|
|
207
|
+
.unwrap_or_default();
|
|
208
|
+
let query = query_from_uri(&parts.uri).unwrap_or_default();
|
|
209
|
+
let cookies = cookies_from_headers(&parts.headers);
|
|
210
|
+
let req_path = parts.uri.path().to_string();
|
|
211
|
+
let request_id = parts
|
|
212
|
+
.extensions
|
|
213
|
+
.get::<RequestId>()
|
|
214
|
+
.map(|id| id.0.clone())
|
|
215
|
+
.unwrap_or_default();
|
|
216
|
+
let auth_claims = parts
|
|
217
|
+
.extensions
|
|
218
|
+
.get::<AuthClaims>()
|
|
219
|
+
.and_then(|ac| ac.0.clone());
|
|
220
|
+
let client_ip = parts
|
|
221
|
+
.extensions
|
|
222
|
+
.get::<ResolvedClientIp>()
|
|
223
|
+
.map(|rci| rci.0.to_string())
|
|
224
|
+
.unwrap_or_default();
|
|
225
|
+
let user_agent = parts
|
|
226
|
+
.extensions
|
|
227
|
+
.get::<ResolvedUserAgent>()
|
|
228
|
+
.map(|ua| ua.0.clone())
|
|
229
|
+
.unwrap_or_default();
|
|
230
|
+
let matched_route = parts
|
|
231
|
+
.extensions
|
|
232
|
+
.get::<MatchedRoute>()
|
|
233
|
+
.map(|mr| mr.0.clone());
|
|
234
|
+
let trace_context = extract_trace_context();
|
|
235
|
+
let request_time = chrono::Utc::now().to_rfc3339();
|
|
236
|
+
|
|
237
|
+
// Read and process the body.
|
|
238
|
+
let content_length = parts
|
|
239
|
+
.headers
|
|
240
|
+
.get("content-length")
|
|
241
|
+
.and_then(|value| value.to_str().ok())
|
|
242
|
+
.and_then(|value| value.parse::<usize>().ok())
|
|
243
|
+
.unwrap_or(0);
|
|
244
|
+
|
|
245
|
+
let (inner_body, content_type) = if content_length > 0 {
|
|
246
|
+
let bytes = axum::body::to_bytes(body, MAX_REQUEST_BODY_SIZE)
|
|
247
|
+
.await
|
|
248
|
+
.map_err(|err| {
|
|
249
|
+
Error::new(
|
|
250
|
+
Status::GenericFailure,
|
|
251
|
+
format!("failed to read request body, {err}"),
|
|
252
|
+
)
|
|
253
|
+
})?;
|
|
254
|
+
let ct_header = parts.headers.get("content-type").cloned();
|
|
255
|
+
let (text_body, binary_body, content_type_str) = to_request_body(&bytes, ct_header);
|
|
256
|
+
let body = if let Some(text) = text_body {
|
|
257
|
+
JsRequestWrapperBody::Text(text)
|
|
258
|
+
} else if let Some(_binary) = binary_body {
|
|
259
|
+
JsRequestWrapperBody::Binary(bytes.to_vec())
|
|
260
|
+
} else {
|
|
261
|
+
JsRequestWrapperBody::EmptyBody
|
|
262
|
+
};
|
|
263
|
+
(body, content_type_str)
|
|
264
|
+
} else {
|
|
265
|
+
(
|
|
266
|
+
JsRequestWrapperBody::EmptyBody,
|
|
267
|
+
parts
|
|
268
|
+
.headers
|
|
269
|
+
.get("content-type")
|
|
270
|
+
.and_then(|v| v.to_str().ok())
|
|
271
|
+
.unwrap_or("")
|
|
272
|
+
.to_string(),
|
|
273
|
+
)
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
Ok(Self {
|
|
277
|
+
inner_parts: parts,
|
|
278
|
+
inner_body,
|
|
279
|
+
path_params,
|
|
280
|
+
query,
|
|
281
|
+
cookies,
|
|
282
|
+
content_type,
|
|
283
|
+
req_path,
|
|
284
|
+
request_id,
|
|
285
|
+
request_time,
|
|
286
|
+
auth_claims,
|
|
287
|
+
client_ip,
|
|
288
|
+
trace_context,
|
|
289
|
+
user_agent,
|
|
290
|
+
matched_route,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// The HTTP version used for the request.
|
|
295
|
+
#[napi(getter)]
|
|
296
|
+
pub fn http_version(&self) -> String {
|
|
297
|
+
format!("{:?}", self.inner_parts.version)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// The HTTP method of the request.
|
|
301
|
+
#[napi(getter)]
|
|
302
|
+
pub fn method(&self) -> String {
|
|
303
|
+
self.inner_parts.method.to_string()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// The URI of the request.
|
|
307
|
+
#[napi(getter)]
|
|
308
|
+
pub fn uri(&self) -> String {
|
|
309
|
+
self.inner_parts.uri.to_string()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// The headers of the request as a map of header name to list of values.
|
|
313
|
+
#[napi(getter)]
|
|
314
|
+
pub fn headers(&self) -> HashMap<String, Vec<String>> {
|
|
315
|
+
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
|
316
|
+
for (key, value) in self.inner_parts.headers.iter() {
|
|
317
|
+
map
|
|
318
|
+
.entry(key.as_str().to_string())
|
|
319
|
+
.or_default()
|
|
320
|
+
.push(value.to_str().unwrap_or_default().to_string());
|
|
321
|
+
}
|
|
322
|
+
map
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// The path of the request (e.g. "/orders/123").
|
|
326
|
+
#[napi(getter)]
|
|
327
|
+
pub fn path(&self) -> String {
|
|
328
|
+
self.req_path.clone()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Path parameters extracted from the URL (e.g. { "orderId": "123" }).
|
|
332
|
+
#[napi(getter)]
|
|
333
|
+
pub fn path_params(&self) -> HashMap<String, String> {
|
|
334
|
+
self.path_params.clone()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// Query parameters, supporting multiple values per key.
|
|
338
|
+
#[napi(getter)]
|
|
339
|
+
pub fn query(&self) -> HashMap<String, Vec<String>> {
|
|
340
|
+
self.query.clone()
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Cookies from the request.
|
|
344
|
+
#[napi(getter)]
|
|
345
|
+
pub fn cookies(&self) -> HashMap<String, String> {
|
|
346
|
+
self.cookies.clone()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// The content type of the request body.
|
|
350
|
+
#[napi(getter)]
|
|
351
|
+
pub fn content_type(&self) -> String {
|
|
352
|
+
self.content_type.clone()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// The request ID (from x-request-id header or auto-generated).
|
|
356
|
+
#[napi(getter)]
|
|
357
|
+
pub fn request_id(&self) -> String {
|
|
358
|
+
self.request_id.clone()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// The request time as an ISO 8601 string.
|
|
362
|
+
#[napi(getter)]
|
|
363
|
+
pub fn request_time(&self) -> String {
|
|
364
|
+
self.request_time.clone()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/// Authentication claims from the auth middleware, or null if no auth.
|
|
368
|
+
#[napi(getter)]
|
|
369
|
+
pub fn auth(&self) -> Option<serde_json::Value> {
|
|
370
|
+
self.auth_claims.clone()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// The client IP address resolved by the runtime.
|
|
374
|
+
#[napi(getter)]
|
|
375
|
+
pub fn client_ip(&self) -> String {
|
|
376
|
+
self.client_ip.clone()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/// The trace context for distributed tracing propagation.
|
|
380
|
+
/// Contains "traceparent" (W3C) and optionally "xray_trace_id" (AWS).
|
|
381
|
+
#[napi(getter)]
|
|
382
|
+
pub fn trace_context(&self) -> Option<HashMap<String, String>> {
|
|
383
|
+
self.trace_context.clone()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// The user-agent string from the request.
|
|
387
|
+
#[napi(getter)]
|
|
388
|
+
pub fn user_agent(&self) -> String {
|
|
389
|
+
self.user_agent.clone()
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// The matched route pattern (e.g. "/orders/{orderId}"), or null if unavailable.
|
|
393
|
+
#[napi(getter)]
|
|
394
|
+
pub fn matched_route(&self) -> Option<String> {
|
|
395
|
+
self.matched_route.clone()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// The text body of the request, or null if the body is empty or binary.
|
|
399
|
+
#[napi(getter)]
|
|
400
|
+
pub fn text_body(&self) -> Option<String> {
|
|
401
|
+
match &self.inner_body {
|
|
402
|
+
JsRequestWrapperBody::Text(text) => Some(text.clone()),
|
|
403
|
+
_ => None,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// The binary body of the request as a Buffer, or null if the body is empty or text.
|
|
408
|
+
#[napi(getter)]
|
|
409
|
+
pub fn binary_body(&self) -> Option<Buffer> {
|
|
410
|
+
match &self.inner_body {
|
|
411
|
+
JsRequestWrapperBody::Binary(bytes) => Some(Buffer::from(bytes.clone())),
|
|
412
|
+
_ => None,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#[napi]
|
|
418
|
+
pub struct CoreRuntimeApplication {
|
|
419
|
+
inner: Application,
|
|
420
|
+
tsfn_cache: Vec<Arc<WeakTsfn>>,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[napi]
|
|
424
|
+
impl CoreRuntimeApplication {
|
|
425
|
+
#[napi(constructor)]
|
|
426
|
+
pub fn new(runtime_config: CoreRuntimeConfig) -> Self {
|
|
427
|
+
let native_runtime_config = RuntimeConfig {
|
|
428
|
+
blueprint_config_path: runtime_config.blueprint_config_path,
|
|
429
|
+
runtime_call_mode: RuntimeCallMode::Ffi,
|
|
430
|
+
server_loopback_only: runtime_config.server_loopback_only,
|
|
431
|
+
server_port: runtime_config.server_port,
|
|
432
|
+
local_api_port: 8259,
|
|
433
|
+
use_custom_health_check: None,
|
|
434
|
+
service_name: "CelerityTestService".to_string(),
|
|
435
|
+
platform: RuntimePlatform::Local,
|
|
436
|
+
trace_otlp_collector_endpoint: "http://localhost:4317".to_string(),
|
|
437
|
+
runtime_max_diagnostics_level: tracing::Level::INFO,
|
|
438
|
+
test_mode: false,
|
|
439
|
+
api_resource: None,
|
|
440
|
+
consumer_app: None,
|
|
441
|
+
schedule_app: None,
|
|
442
|
+
resource_store_verify_tls: true,
|
|
443
|
+
resource_store_cache_entry_ttl: 600,
|
|
444
|
+
resource_store_cleanup_interval: 3600,
|
|
445
|
+
client_ip_source: ClientIpSource::ConnectInfo,
|
|
446
|
+
};
|
|
447
|
+
let inner = Application::new(native_runtime_config, Box::new(ProcessEnvVars::new()));
|
|
448
|
+
CoreRuntimeApplication {
|
|
449
|
+
inner,
|
|
450
|
+
tsfn_cache: vec![],
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
#[napi]
|
|
455
|
+
pub fn setup(&mut self) -> Result<CoreRuntimeAppConfig> {
|
|
456
|
+
let app_config = self.inner.setup().map_err(|err| {
|
|
457
|
+
Error::new(
|
|
458
|
+
Status::GenericFailure,
|
|
459
|
+
format!("failed to setup core runtime, {err}"),
|
|
460
|
+
)
|
|
461
|
+
})?;
|
|
462
|
+
Ok(app_config.into())
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#[napi]
|
|
466
|
+
pub fn register_http_handler(
|
|
467
|
+
&mut self,
|
|
468
|
+
path: String,
|
|
469
|
+
method: String,
|
|
470
|
+
timeout_seconds: Option<i64>,
|
|
471
|
+
#[napi(ts_arg_type = "(err: Error | null, request: Request) => Promise<Response>")]
|
|
472
|
+
handler: WeakTsfn,
|
|
473
|
+
) -> Result<()> {
|
|
474
|
+
let tsfn = Arc::new(handler);
|
|
475
|
+
self.tsfn_cache.push(tsfn.clone());
|
|
476
|
+
let timeout_secs = timeout_seconds.unwrap_or(60) as u64;
|
|
477
|
+
|
|
478
|
+
let handler = move |req| {
|
|
479
|
+
let tsfn = tsfn.clone();
|
|
480
|
+
async move {
|
|
481
|
+
let js_req_wrapper = JsRequestWrapper::from_axum_req(req)
|
|
482
|
+
.await
|
|
483
|
+
.map_err(|err| HandlerError::new(err.to_string()))?;
|
|
484
|
+
let promise = tsfn
|
|
485
|
+
.call_async(Ok(js_req_wrapper))
|
|
486
|
+
.await
|
|
487
|
+
.map_err(|err| HandlerError::new(err.to_string()))?;
|
|
488
|
+
let sleep = time::sleep(Duration::from_secs(timeout_secs));
|
|
489
|
+
tokio::select! {
|
|
490
|
+
_ = sleep => {
|
|
491
|
+
Err(HandlerError::timeout())
|
|
492
|
+
}
|
|
493
|
+
value = promise => {
|
|
494
|
+
Ok::<Response, HandlerError>(value.map_err(|err| HandlerError::new(err.to_string()))?)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
self.inner.register_http_handler(&path, &method, handler);
|
|
500
|
+
Ok(())
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[allow(clippy::missing_safety_doc)]
|
|
504
|
+
#[napi]
|
|
505
|
+
pub async unsafe fn run(&mut self, block: bool) -> Result<()> {
|
|
506
|
+
self.inner.run(block).await.map_err(|err| {
|
|
507
|
+
Error::new(
|
|
508
|
+
Status::GenericFailure,
|
|
509
|
+
format!("failed to start core runtime, {err}"),
|
|
510
|
+
)
|
|
511
|
+
})?;
|
|
512
|
+
Ok(())
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[napi]
|
|
516
|
+
pub fn shutdown(&mut self) -> Result<()> {
|
|
517
|
+
self.inner.shutdown();
|
|
518
|
+
self.tsfn_cache.clear();
|
|
519
|
+
Ok(())
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
524
|
+
pub struct HandlerError {
|
|
525
|
+
pub message: String,
|
|
526
|
+
#[serde(skip)]
|
|
527
|
+
pub is_timeout: bool,
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
impl HandlerError {
|
|
531
|
+
pub fn new(message: String) -> Self {
|
|
532
|
+
Self {
|
|
533
|
+
message,
|
|
534
|
+
is_timeout: false,
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
pub fn timeout() -> Self {
|
|
539
|
+
Self {
|
|
540
|
+
message: "handler timed out".to_string(),
|
|
541
|
+
is_timeout: true,
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
impl std::fmt::Display for HandlerError {
|
|
547
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
548
|
+
write!(f, "{}", self.message)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
impl IntoResponse for HandlerError {
|
|
553
|
+
fn into_response(self) -> axum::response::Response<Body> {
|
|
554
|
+
let status = if self.is_timeout {
|
|
555
|
+
StatusCode::GATEWAY_TIMEOUT
|
|
556
|
+
} else {
|
|
557
|
+
StatusCode::INTERNAL_SERVER_ERROR
|
|
558
|
+
};
|
|
559
|
+
(status, axum::response::Json(self)).into_response()
|
|
560
|
+
}
|
|
561
|
+
}
|
package/tsconfig.json
ADDED