@weft-js/start 0.1.0 → 0.1.2
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/README.md +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +51 -10
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +97 -168
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +113 -35
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -174
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -964
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
//! `native()` (PLAN §4.3, research 39): a service's functions in-process. One crossing per function, as with a sidecar,
|
|
2
|
+
//! with no HTTP and no signature: nothing outside the process can reach it.
|
|
3
|
+
//!
|
|
4
|
+
//! A real addon runs each call on the host's worker pool, so a long one doesn't hold the event loop. A `.wasm` is the
|
|
5
|
+
//! same code behind a small ABI of its own, on whatever thread calls it. Neither brings an async runtime: this is for
|
|
6
|
+
//! CPU, and a function that waits on IO belongs in `remote()`.
|
|
7
|
+
|
|
8
|
+
use std::future::Future;
|
|
9
|
+
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
10
|
+
use std::pin::pin;
|
|
11
|
+
use std::sync::OnceLock;
|
|
12
|
+
use std::task::{Context, Poll, Waker};
|
|
13
|
+
|
|
14
|
+
use serde_json::json;
|
|
15
|
+
|
|
16
|
+
use crate::ctx::{Base, Envelope};
|
|
17
|
+
use crate::functions::{Answer, Failed, Functions, drift_body, internal_body, ok_body, panic_text};
|
|
18
|
+
|
|
19
|
+
static MAKE: OnceLock<fn() -> Functions> = OnceLock::new();
|
|
20
|
+
static FUNCTIONS: OnceLock<Functions> = OnceLock::new();
|
|
21
|
+
|
|
22
|
+
/// Called once by `weft::native!`. The service is built on the first call, not while the library loads.
|
|
23
|
+
pub fn install(make: fn() -> Functions) {
|
|
24
|
+
let _ = MAKE.set(make);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn functions() -> &'static Functions {
|
|
28
|
+
FUNCTIONS.get_or_init(|| MAKE.get().expect("weft::native! ran")())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// `{ service, hash, functions }`: TS refuses a library built from another contract (R103).
|
|
32
|
+
fn contract() -> String {
|
|
33
|
+
let f = functions();
|
|
34
|
+
json!({ "service": f.service(), "hash": f.hash(), "functions": f.ids() }).to_string()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[cfg(not(target_family = "wasm"))]
|
|
38
|
+
fn wait<F: Future>(mut future: std::pin::Pin<&mut F>) -> F::Output {
|
|
39
|
+
struct Unpark(std::thread::Thread);
|
|
40
|
+
impl std::task::Wake for Unpark {
|
|
41
|
+
fn wake(self: std::sync::Arc<Self>) {
|
|
42
|
+
self.0.unpark();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let waker = Waker::from(std::sync::Arc::new(Unpark(std::thread::current())));
|
|
46
|
+
let mut cx = Context::from_waker(&waker);
|
|
47
|
+
loop {
|
|
48
|
+
match future.as_mut().poll(&mut cx) {
|
|
49
|
+
Poll::Ready(v) => return v,
|
|
50
|
+
Poll::Pending => std::thread::park(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[cfg(target_family = "wasm")]
|
|
56
|
+
fn wait<F: Future>(future: std::pin::Pin<&mut F>) -> F::Output {
|
|
57
|
+
match future.poll(&mut Context::from_waker(Waker::noop())) {
|
|
58
|
+
Poll::Ready(v) => v,
|
|
59
|
+
Poll::Pending => {
|
|
60
|
+
panic!(
|
|
61
|
+
"a `native()` function can't wait on WASM: nothing would wake it. Use `remote()` for IO"
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// One call, start to finish, on this thread.
|
|
68
|
+
fn dispatch(id: &str, ctx: &str, body: Vec<u8>) -> String {
|
|
69
|
+
let envelope: Envelope = match serde_json::from_str(ctx) {
|
|
70
|
+
Ok(e) => e,
|
|
71
|
+
Err(e) => return drift_body(&format!("the call's context didn't parse: {e}")).to_string(),
|
|
72
|
+
};
|
|
73
|
+
let Some(handler) = functions().handler(id) else {
|
|
74
|
+
return drift_body(&format!(
|
|
75
|
+
"this library has no function `{id}`: rebuild it from the current contract"
|
|
76
|
+
))
|
|
77
|
+
.to_string();
|
|
78
|
+
};
|
|
79
|
+
let base = Base::new(id, envelope);
|
|
80
|
+
let call = handler(base.clone(), body);
|
|
81
|
+
let answer = catch_unwind(AssertUnwindSafe(|| wait(pin!(call)))).unwrap_or_else(|p| {
|
|
82
|
+
Answer::Failed(Failed::Internal(format!("panicked: {}", panic_text(&*p))))
|
|
83
|
+
});
|
|
84
|
+
match answer {
|
|
85
|
+
Answer::Value(value) => ok_body(&value, &base),
|
|
86
|
+
Answer::Failed(Failed::Typed(error)) => json!({ "ok": false, "error": error }).to_string(),
|
|
87
|
+
Answer::Failed(Failed::Internal(detail)) => internal_body(&base, Some(&detail)).to_string(),
|
|
88
|
+
Answer::Drift(detail) => drift_body(&detail).to_string(),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[cfg(not(target_family = "wasm"))]
|
|
93
|
+
mod addon {
|
|
94
|
+
use napi::bindgen_prelude::{AsyncTask, Env, Result, Task};
|
|
95
|
+
use napi_derive::napi;
|
|
96
|
+
|
|
97
|
+
pub struct Call {
|
|
98
|
+
id: String,
|
|
99
|
+
ctx: String,
|
|
100
|
+
body: Option<String>,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl Task for Call {
|
|
104
|
+
type Output = String;
|
|
105
|
+
type JsValue = String;
|
|
106
|
+
|
|
107
|
+
fn compute(&mut self) -> Result<String> {
|
|
108
|
+
Ok(super::dispatch(
|
|
109
|
+
&self.id,
|
|
110
|
+
&self.ctx,
|
|
111
|
+
self.body.take().unwrap_or_default().into_bytes(),
|
|
112
|
+
))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn resolve(&mut self, _: Env, reply: String) -> Result<String> {
|
|
116
|
+
Ok(reply)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[napi]
|
|
121
|
+
pub fn call(id: String, ctx: String, body: String) -> AsyncTask<Call> {
|
|
122
|
+
AsyncTask::new(Call {
|
|
123
|
+
id,
|
|
124
|
+
ctx,
|
|
125
|
+
body: Some(body),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[napi]
|
|
130
|
+
pub fn contract() -> String {
|
|
131
|
+
super::contract()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// A pointer and a length in one number: the only return value this ABI needs, and it fits a JS `BigInt`.
|
|
136
|
+
#[cfg(target_family = "wasm")]
|
|
137
|
+
mod wasm {
|
|
138
|
+
#[link(wasm_import_module = "weft")]
|
|
139
|
+
unsafe extern "C" {
|
|
140
|
+
fn now() -> f64;
|
|
141
|
+
fn panicked(ptr: *const u8, len: usize);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn give(text: String) -> u64 {
|
|
145
|
+
let bytes = text.into_bytes().into_boxed_slice();
|
|
146
|
+
let len = bytes.len() as u64;
|
|
147
|
+
((Box::into_raw(bytes).cast::<u8>() as u64) << 32) | len
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// # Safety
|
|
151
|
+
/// `ptr` and `len` are valid UTF-8 that outlives `'a`.
|
|
152
|
+
unsafe fn text<'a>(ptr: *const u8, len: usize) -> &'a str {
|
|
153
|
+
// SAFETY: the caller's contract above.
|
|
154
|
+
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len)) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[unsafe(no_mangle)]
|
|
158
|
+
pub extern "C" fn weft_alloc(len: usize) -> *mut u8 {
|
|
159
|
+
Box::into_raw(vec![0u8; len].into_boxed_slice()).cast::<u8>()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// # Safety
|
|
163
|
+
/// `ptr` and `len` are what `weft_alloc`, `weft_call` or `weft_contract` handed out.
|
|
164
|
+
#[unsafe(no_mangle)]
|
|
165
|
+
pub unsafe extern "C" fn weft_free(ptr: *mut u8, len: usize) {
|
|
166
|
+
// SAFETY: the caller hands back a boxed slice of exactly `len` bytes.
|
|
167
|
+
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// `body` is the caller's, and is taken over, as `weft_alloc` gave it.
|
|
171
|
+
///
|
|
172
|
+
/// # Safety
|
|
173
|
+
/// Each pair is valid UTF-8 from `weft_alloc`.
|
|
174
|
+
#[unsafe(no_mangle)]
|
|
175
|
+
pub unsafe extern "C" fn weft_call(
|
|
176
|
+
id: *const u8,
|
|
177
|
+
id_len: usize,
|
|
178
|
+
ctx: *const u8,
|
|
179
|
+
ctx_len: usize,
|
|
180
|
+
body: *mut u8,
|
|
181
|
+
body_len: usize,
|
|
182
|
+
) -> u64 {
|
|
183
|
+
// A panic aborts here: the instance traps, and TS starts a new one. This is how the message gets out first.
|
|
184
|
+
static HOOK: std::sync::Once = std::sync::Once::new();
|
|
185
|
+
HOOK.call_once(|| {
|
|
186
|
+
std::panic::set_hook(Box::new(|info| {
|
|
187
|
+
let message = info.to_string();
|
|
188
|
+
// SAFETY: the host copies the bytes out during the call; `message` is live and UTF-8.
|
|
189
|
+
unsafe { panicked(message.as_ptr(), message.len()) }
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
// SAFETY: `body` is a `weft_alloc` box of `body_len` bytes, and it's ours now.
|
|
193
|
+
let body =
|
|
194
|
+
unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(body, body_len)) }.into_vec();
|
|
195
|
+
// SAFETY: the caller keeps both strings, valid UTF-8, for the whole call.
|
|
196
|
+
let (id, ctx) = unsafe { (text(id, id_len), text(ctx, ctx_len)) };
|
|
197
|
+
give(super::dispatch(id, ctx, body))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[unsafe(no_mangle)]
|
|
201
|
+
pub extern "C" fn weft_contract() -> u64 {
|
|
202
|
+
give(super::contract())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
pub(crate) fn time() -> f64 {
|
|
206
|
+
// SAFETY: a plain import: no arguments, no memory.
|
|
207
|
+
unsafe { now() }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[cfg(target_family = "wasm")]
|
|
212
|
+
pub(crate) use wasm::time as now_ms;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//! With the `otel` feature, a call's span continues the trace TS started: the app installs an OpenTelemetry layer, and
|
|
2
|
+
//! the envelope's `traceparent` becomes the parent. Without one, `trace_id` and `parent_id` are plain span fields.
|
|
3
|
+
|
|
4
|
+
use opentelemetry::Context;
|
|
5
|
+
use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState};
|
|
6
|
+
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
|
7
|
+
|
|
8
|
+
pub(crate) fn parent(traceparent: &str) -> Option<SpanContext> {
|
|
9
|
+
let mut parts = traceparent.split('-');
|
|
10
|
+
let (version, trace, span, flags) =
|
|
11
|
+
(parts.next()?, parts.next()?, parts.next()?, parts.next()?);
|
|
12
|
+
if version != "00" {
|
|
13
|
+
return None;
|
|
14
|
+
}
|
|
15
|
+
let trace = TraceId::from_hex(trace)
|
|
16
|
+
.ok()
|
|
17
|
+
.filter(|t| *t != TraceId::INVALID)?;
|
|
18
|
+
let span = SpanId::from_hex(span)
|
|
19
|
+
.ok()
|
|
20
|
+
.filter(|s| *s != SpanId::INVALID)?;
|
|
21
|
+
let flags = TraceFlags::new(u8::from_str_radix(flags, 16).ok()?);
|
|
22
|
+
Some(SpanContext::new(
|
|
23
|
+
trace,
|
|
24
|
+
span,
|
|
25
|
+
flags,
|
|
26
|
+
true,
|
|
27
|
+
TraceState::default(),
|
|
28
|
+
))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub(crate) fn link(span: &tracing::Span, traceparent: Option<&str>) {
|
|
32
|
+
if let Some(remote) = traceparent.and_then(parent) {
|
|
33
|
+
let _ = span.set_parent(Context::new().with_remote_span_context(remote));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[cfg(test)]
|
|
38
|
+
mod tests {
|
|
39
|
+
use super::*;
|
|
40
|
+
|
|
41
|
+
#[test]
|
|
42
|
+
fn reads_a_w3c_traceparent() {
|
|
43
|
+
let p = parent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").unwrap();
|
|
44
|
+
assert_eq!(p.trace_id().to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
|
|
45
|
+
assert_eq!(p.span_id().to_string(), "00f067aa0ba902b7");
|
|
46
|
+
assert!(p.is_sampled());
|
|
47
|
+
assert!(p.is_remote());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn refuses_what_isnt_one() {
|
|
52
|
+
for bad in [
|
|
53
|
+
"",
|
|
54
|
+
"junk",
|
|
55
|
+
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
|
56
|
+
"00-00000000000000000000000000000000-00f067aa0ba902b7-01",
|
|
57
|
+
"00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
|
|
58
|
+
] {
|
|
59
|
+
assert!(parent(bad).is_none(), "{bad}");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
//! `weft::router()`: an axum `Router`, so a sidecar can be one binary or part of a Loco or axum app. Each call is
|
|
2
|
+
//! verified, deserialized, run in its own task with a deadline, and answered as
|
|
3
|
+
//!
|
|
4
|
+
//! ```text
|
|
5
|
+
//! { "ok": true, "value": …, "tags": […], "touched": […] }
|
|
6
|
+
//! { "ok": false, "error": { "code": "NotFound", "data": … } }
|
|
7
|
+
//! ```
|
|
8
|
+
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
11
|
+
|
|
12
|
+
use axum::Router;
|
|
13
|
+
use axum::body::Bytes;
|
|
14
|
+
use axum::extract::{Path, State};
|
|
15
|
+
use axum::http::{HeaderMap, StatusCode, header};
|
|
16
|
+
use axum::response::{IntoResponse, Response};
|
|
17
|
+
use axum::routing::{get, post};
|
|
18
|
+
use serde_json::{Value, json};
|
|
19
|
+
use tracing::Instrument;
|
|
20
|
+
|
|
21
|
+
use crate::ctx::Base;
|
|
22
|
+
use crate::envelope::{Envelope, verify};
|
|
23
|
+
use crate::functions::{Answer, Failed, Functions, drift_body, internal_body, ok_body};
|
|
24
|
+
|
|
25
|
+
struct Shared {
|
|
26
|
+
functions: Functions,
|
|
27
|
+
secret: Vec<u8>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// The `Router`: `POST /_weft/fn/:id`, and `GET /_weft/health` for the supervisor. The key comes from `WEFT_RUST_SECRET`.
|
|
31
|
+
///
|
|
32
|
+
/// # Panics
|
|
33
|
+
/// With no `WEFT_RUST_SECRET`: a sidecar that took unsigned calls would trust any process that could reach it.
|
|
34
|
+
pub fn router(functions: Functions) -> Router {
|
|
35
|
+
let secret = std::env::var("WEFT_RUST_SECRET").unwrap_or_default();
|
|
36
|
+
assert!(
|
|
37
|
+
!secret.is_empty(),
|
|
38
|
+
"R106: WEFT_RUST_SECRET is not set. `weft dev` and `weft start` set it; a sidecar deployed on its own needs the same value as the server"
|
|
39
|
+
);
|
|
40
|
+
router_with(functions, secret.into_bytes())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// [`router`] with the key in hand.
|
|
44
|
+
pub fn router_with(functions: Functions, secret: Vec<u8>) -> Router {
|
|
45
|
+
Router::new()
|
|
46
|
+
.route("/_weft/fn/{id}", post(call))
|
|
47
|
+
.route("/_weft/health", get(health))
|
|
48
|
+
.with_state(Arc::new(Shared { functions, secret }))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async fn health(State(s): State<Arc<Shared>>) -> Response {
|
|
52
|
+
json_response(
|
|
53
|
+
StatusCode::OK,
|
|
54
|
+
&json!({ "ok": true, "service": s.functions.service(), "contract": s.functions.hash(), "functions": s.functions.ids(), "pid": std::process::id() }),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn json_response(status: StatusCode, body: &Value) -> Response {
|
|
59
|
+
text_response(status, body.to_string())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn text_response(status: StatusCode, body: String) -> Response {
|
|
63
|
+
(status, [(header::CONTENT_TYPE, "application/json")], body).into_response()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn refused(status: StatusCode, code: &str, message: impl std::fmt::Display) -> Response {
|
|
67
|
+
json_response(
|
|
68
|
+
status,
|
|
69
|
+
&json!({ "ok": false, "error": { "code": code, "message": message.to_string() } }),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn now_ms() -> u64 {
|
|
74
|
+
SystemTime::now()
|
|
75
|
+
.duration_since(UNIX_EPOCH)
|
|
76
|
+
.map_or(0, |d| d.as_millis() as u64)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// `00-<trace>-<span>-<flags>` split, or nothing if it isn't one.
|
|
80
|
+
fn trace_ids(traceparent: Option<&str>) -> (&str, &str) {
|
|
81
|
+
let mut parts = traceparent.unwrap_or("").split('-');
|
|
82
|
+
match (parts.next(), parts.next(), parts.next()) {
|
|
83
|
+
(Some("00"), Some(trace), Some(span)) if trace.len() == 32 && span.len() == 16 => {
|
|
84
|
+
(trace, span)
|
|
85
|
+
}
|
|
86
|
+
_ => ("", ""),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Dropping this stops the task: axum drops the handler's future when TS hangs up.
|
|
91
|
+
struct AbortOnDrop(tokio::task::AbortHandle);
|
|
92
|
+
impl Drop for AbortOnDrop {
|
|
93
|
+
fn drop(&mut self) {
|
|
94
|
+
self.0.abort();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async fn call(
|
|
99
|
+
State(s): State<Arc<Shared>>,
|
|
100
|
+
Path(id): Path<String>,
|
|
101
|
+
headers: HeaderMap,
|
|
102
|
+
body: Bytes,
|
|
103
|
+
) -> Response {
|
|
104
|
+
let text = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
|
|
105
|
+
let (Some(ctx), Some(sig)) = (text("weft-ctx"), text("weft-sig")) else {
|
|
106
|
+
return refused(
|
|
107
|
+
StatusCode::UNAUTHORIZED,
|
|
108
|
+
"Refused",
|
|
109
|
+
crate::envelope::Refused::Malformed,
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
let envelope: Envelope = match verify(&s.secret, &id, ctx, sig, &body, now_ms()) {
|
|
113
|
+
Ok(e) => e,
|
|
114
|
+
Err(e) => return refused(StatusCode::UNAUTHORIZED, "Refused", e),
|
|
115
|
+
};
|
|
116
|
+
let Some(handler) = s.functions.handler(&id) else {
|
|
117
|
+
return refused(
|
|
118
|
+
StatusCode::NOT_FOUND,
|
|
119
|
+
"Drift",
|
|
120
|
+
format!("this sidecar has no function `{id}`: rebuild it from the current contract"),
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
let budget = Duration::from_millis(envelope.deadline_ms);
|
|
124
|
+
let base = Base::new(&id, envelope);
|
|
125
|
+
let (trace_id, parent_id) = trace_ids(base.traceparent());
|
|
126
|
+
let span = tracing::info_span!(
|
|
127
|
+
"weft.fn",
|
|
128
|
+
otel.name = id.as_str(),
|
|
129
|
+
function = id.as_str(),
|
|
130
|
+
request_id = base.request_id(),
|
|
131
|
+
trace_id,
|
|
132
|
+
parent_id
|
|
133
|
+
);
|
|
134
|
+
#[cfg(feature = "otel")]
|
|
135
|
+
crate::otel::link(&span, base.traceparent());
|
|
136
|
+
|
|
137
|
+
// A task of its own, so a panic is one failed call and not a dropped connection.
|
|
138
|
+
let task = tokio::spawn(handler(base.clone(), Vec::from(body)).instrument(span.clone()));
|
|
139
|
+
let _abort = AbortOnDrop(task.abort_handle());
|
|
140
|
+
let outcome = tokio::time::timeout(budget, task).await;
|
|
141
|
+
let _enter = span.enter();
|
|
142
|
+
let answer = match outcome {
|
|
143
|
+
Ok(Ok(answer)) => answer,
|
|
144
|
+
Ok(Err(e)) if e.is_panic() => Answer::Failed(Failed::Internal(format!(
|
|
145
|
+
"panicked: {}",
|
|
146
|
+
crate::functions::panic_text(&*e.into_panic())
|
|
147
|
+
))),
|
|
148
|
+
Ok(Err(e)) => Answer::Failed(Failed::Internal(e.to_string())),
|
|
149
|
+
Err(_) => {
|
|
150
|
+
tracing::warn!(budget_ms = budget.as_millis() as u64, "deadline passed");
|
|
151
|
+
return refused(
|
|
152
|
+
StatusCode::GATEWAY_TIMEOUT,
|
|
153
|
+
"Deadline",
|
|
154
|
+
format!("`{id}` ran past its deadline"),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
match answer {
|
|
159
|
+
Answer::Value(value) => text_response(StatusCode::OK, ok_body(&value, &base)),
|
|
160
|
+
Answer::Failed(Failed::Typed(error)) => {
|
|
161
|
+
json_response(StatusCode::OK, &json!({ "ok": false, "error": error }))
|
|
162
|
+
}
|
|
163
|
+
Answer::Failed(Failed::Internal(detail)) => {
|
|
164
|
+
tracing::error!(detail, "failed");
|
|
165
|
+
json_response(
|
|
166
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
167
|
+
&internal_body(&base, None),
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
Answer::Drift(detail) => {
|
|
171
|
+
tracing::error!(detail, "drift");
|
|
172
|
+
json_response(StatusCode::UNPROCESSABLE_ENTITY, &drift_body(&detail))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Serve on `WEFT_LISTEN` (`127.0.0.1:0` when unset), print `weft-listening <addr>` for the supervisor, and drain on
|
|
178
|
+
/// SIGTERM or Ctrl-C. Logs go to stderr, by `RUST_LOG` (`info`), unless the app installed a subscriber first.
|
|
179
|
+
pub async fn serve(router: Router) -> std::io::Result<()> {
|
|
180
|
+
use tracing_subscriber::EnvFilter;
|
|
181
|
+
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
|
182
|
+
let _ = tracing_subscriber::fmt()
|
|
183
|
+
.with_env_filter(filter)
|
|
184
|
+
.with_writer(std::io::stderr)
|
|
185
|
+
.try_init();
|
|
186
|
+
let addr = std::env::var("WEFT_LISTEN").unwrap_or_else(|_| "127.0.0.1:0".into());
|
|
187
|
+
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
|
188
|
+
println!("weft-listening {}", listener.local_addr()?);
|
|
189
|
+
axum::serve(listener, router)
|
|
190
|
+
.with_graceful_shutdown(stopped())
|
|
191
|
+
.await
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async fn stopped() {
|
|
195
|
+
#[cfg(unix)]
|
|
196
|
+
{
|
|
197
|
+
use tokio::signal::unix::{SignalKind, signal};
|
|
198
|
+
let mut term = signal(SignalKind::terminate()).expect("a SIGTERM handler");
|
|
199
|
+
tokio::select! { _ = term.recv() => {}, _ = tokio::signal::ctrl_c() => {} }
|
|
200
|
+
}
|
|
201
|
+
#[cfg(not(unix))]
|
|
202
|
+
let _ = tokio::signal::ctrl_c().await;
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/start",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "weft Start: file routes, server functions, streaming SSR, a database and auth",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"weft"
|
|
7
|
+
],
|
|
5
8
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"@takumi-rs/helpers": "2.18.2",
|
|
11
|
-
"@takumi-rs/wasm": "2.18.2",
|
|
12
|
-
"@tanstack/history": "1.162.5",
|
|
13
|
-
"@tanstack/router-core": "1.171.39",
|
|
14
|
-
"nitro": "3.0.260903-beta",
|
|
15
|
-
"vite": "8.3.5"
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Ravonus/weft.git",
|
|
12
|
+
"directory": "packages/start"
|
|
16
13
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"drizzle-zod": "^0.8.0",
|
|
21
|
-
"mysql2": "^3.24.0",
|
|
22
|
-
"postgres": ">=3",
|
|
23
|
-
"@weft-js/core": "^0.1.0",
|
|
24
|
-
"zod": "^4.0.0"
|
|
14
|
+
"homepage": "https://github.com/Ravonus/weft/tree/main/packages/start#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Ravonus/weft/issues"
|
|
25
17
|
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"crate",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": [
|
|
26
|
+
"**/*.css",
|
|
27
|
+
"./src/share.ts"
|
|
28
|
+
],
|
|
26
29
|
"exports": {
|
|
27
30
|
".": "./src/index.ts",
|
|
28
31
|
"./data": "./src/data.ts",
|
|
@@ -61,10 +64,28 @@
|
|
|
61
64
|
"./rust": "./src/rust.ts",
|
|
62
65
|
"./sidecar": "./src/sidecar.mjs"
|
|
63
66
|
},
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@fontsource/inter": "5.3.0",
|
|
72
|
+
"@takumi-rs/core": "2.18.2",
|
|
73
|
+
"@takumi-rs/helpers": "2.18.2",
|
|
74
|
+
"@takumi-rs/wasm": "2.18.2",
|
|
75
|
+
"@tanstack/history": "1.162.5",
|
|
76
|
+
"@tanstack/router-core": "1.171.39",
|
|
77
|
+
"nitro": "3.0.260903-beta",
|
|
78
|
+
"vite": "8.3.5"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@weft-js/core": "^0.1.2",
|
|
82
|
+
"better-auth": "^1.7.0",
|
|
83
|
+
"drizzle-orm": "^0.45.0",
|
|
84
|
+
"drizzle-zod": "^0.8.0",
|
|
85
|
+
"mysql2": "^3.24.0",
|
|
86
|
+
"postgres": ">=3",
|
|
87
|
+
"zod": "^4.0.0"
|
|
88
|
+
},
|
|
68
89
|
"peerDependenciesMeta": {
|
|
69
90
|
"better-auth": {
|
|
70
91
|
"optional": true
|
|
@@ -91,19 +112,7 @@
|
|
|
91
112
|
"optional": true
|
|
92
113
|
}
|
|
93
114
|
},
|
|
94
|
-
"publishConfig": {
|
|
95
|
-
"access": "public"
|
|
96
|
-
},
|
|
97
115
|
"engines": {
|
|
98
116
|
"node": ">=26"
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
"weft"
|
|
102
|
-
],
|
|
103
|
-
"files": [
|
|
104
|
-
"src",
|
|
105
|
-
"crate",
|
|
106
|
-
"README.md",
|
|
107
|
-
"LICENSE"
|
|
108
|
-
]
|
|
109
|
-
}
|
|
117
|
+
}
|
|
118
|
+
}
|