@weft-js/start 0.1.1 → 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.
Files changed (213) hide show
  1. package/README.md +36 -3
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/rust-toolchain.toml +3 -0
  5. package/crate/src/build/emit.rs +103 -0
  6. package/crate/src/build.rs +367 -0
  7. package/crate/src/ctx.rs +186 -0
  8. package/crate/src/envelope.rs +163 -0
  9. package/crate/src/functions.rs +173 -0
  10. package/crate/src/lib.rs +118 -0
  11. package/crate/src/native.rs +212 -0
  12. package/crate/src/otel.rs +62 -0
  13. package/crate/src/server.rs +203 -0
  14. package/package.json +46 -37
  15. package/src/app.mjs +38 -8
  16. package/src/asset-receipt.d.mts +13 -0
  17. package/src/asset-receipt.mjs +61 -0
  18. package/src/auth/better-auth.ts +215 -17
  19. package/src/auth/drizzle.ts +1 -1
  20. package/src/auth.ts +21 -6
  21. package/src/boot.ts +19 -8
  22. package/src/bus/postgres.ts +32 -19
  23. package/src/bus.ts +7 -4
  24. package/src/{capture.mjs → capture.ts} +34 -1
  25. package/src/changes-sql.d.mts +8 -2
  26. package/src/changes-sql.mjs +26 -10
  27. package/src/changes.ts +165 -66
  28. package/src/channel/connections.ts +216 -0
  29. package/src/channel/socket.ts +396 -0
  30. package/src/channel-fake.ts +19 -4
  31. package/src/channel.ts +120 -492
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +144 -157
  35. package/src/data/cache.ts +354 -0
  36. package/src/data/definitions.ts +94 -0
  37. package/src/data/draft.ts +104 -0
  38. package/src/data/foundation.ts +95 -0
  39. package/src/data/live-api.ts +22 -0
  40. package/src/data/mutation.ts +138 -0
  41. package/src/data/primitives.ts +135 -0
  42. package/src/data/query.ts +106 -0
  43. package/src/data/transport.ts +121 -0
  44. package/src/data/types.ts +303 -0
  45. package/src/data.ts +69 -894
  46. package/src/db/bun-sqlite.d.ts +4 -1
  47. package/src/db/changes-mysql.ts +61 -21
  48. package/src/db/changes-postgres.ts +197 -118
  49. package/src/db/changes-sqlite.ts +131 -64
  50. package/src/db/core.ts +38 -13
  51. package/src/db/index.ts +20 -4
  52. package/src/db/mysql.ts +22 -6
  53. package/src/db/postgres.ts +49 -13
  54. package/src/db/sqlite.ts +53 -27
  55. package/src/derive.d.mts +4 -1
  56. package/src/derive.mjs +136 -39
  57. package/src/dev-runtime.mjs +8 -4
  58. package/src/exec/errors.ts +57 -0
  59. package/src/exec/landing.ts +66 -0
  60. package/src/exec/scope.ts +174 -0
  61. package/src/exec/touch.ts +80 -0
  62. package/src/exec/validation.ts +176 -0
  63. package/src/exec.ts +191 -341
  64. package/src/extension.ts +8 -2
  65. package/src/first-paint.ts +26 -9
  66. package/src/form/value.ts +15 -0
  67. package/src/form.tsx +132 -43
  68. package/src/hub/channels.ts +102 -0
  69. package/src/hub/connections.ts +147 -0
  70. package/src/hub/delivery.ts +62 -0
  71. package/src/hub/feed.ts +168 -0
  72. package/src/hub/presence.ts +64 -0
  73. package/src/hub/route.ts +158 -0
  74. package/src/hub/subscriptions.ts +212 -0
  75. package/src/hub/topic.ts +98 -0
  76. package/src/hub/tracked.ts +12 -0
  77. package/src/hub/types.ts +190 -0
  78. package/src/hub/view.ts +147 -0
  79. package/src/hub.ts +132 -758
  80. package/src/index.ts +84 -10
  81. package/src/inline.mjs +356 -0
  82. package/src/inspect/compare.mjs +162 -0
  83. package/src/inspect/routes.mjs +154 -0
  84. package/src/inspect/samples.mjs +59 -0
  85. package/src/inspect/url.mjs +2 -0
  86. package/src/inspect.mjs +146 -187
  87. package/src/link/election.ts +178 -0
  88. package/src/link/lines.ts +27 -0
  89. package/src/link.ts +90 -166
  90. package/src/list-drizzle.ts +104 -18
  91. package/src/list.ts +127 -36
  92. package/src/listen.ts +32 -6
  93. package/src/live/api.ts +18 -0
  94. package/src/live/backoff.ts +6 -0
  95. package/src/live/frames.ts +42 -0
  96. package/src/live/read.ts +77 -0
  97. package/src/live/sub-types.ts +24 -0
  98. package/src/live/transport.ts +86 -0
  99. package/src/live/types.ts +52 -0
  100. package/src/live-fake.ts +20 -5
  101. package/src/live-target.mjs +112 -37
  102. package/src/live.ts +116 -225
  103. package/src/media.ts +8 -1
  104. package/src/migrations.mjs +14 -6
  105. package/src/mux.ts +161 -64
  106. package/src/og/DefaultCard.tsx +40 -9
  107. package/src/og/fonts.mjs +16 -8
  108. package/src/og/images.mjs +43 -22
  109. package/src/og/index.mjs +119 -43
  110. package/src/og/native.mjs +11 -4
  111. package/src/range.mjs +15 -3
  112. package/src/router.ts +56 -6
  113. package/src/routes.mjs +73 -22
  114. package/src/run/admission.ts +80 -0
  115. package/src/run/boot.tsx +115 -0
  116. package/src/run/cache-presentation.ts +67 -0
  117. package/src/run/cache.ts +254 -0
  118. package/src/run/helpers.ts +197 -0
  119. package/src/run/listeners.ts +155 -0
  120. package/src/run/navigation.ts +382 -0
  121. package/src/run/page-skeleton.tsx +35 -0
  122. package/src/run/prefetch.ts +128 -0
  123. package/src/run/retain-view.ts +18 -0
  124. package/src/run/shape-preparation.ts +97 -0
  125. package/src/run/shape-styles.ts +72 -0
  126. package/src/run/source-context/pin.ts +124 -0
  127. package/src/run/source-context.ts +311 -0
  128. package/src/run.tsx +50 -178
  129. package/src/rust/common.ts +163 -0
  130. package/src/rust/native.ts +282 -0
  131. package/src/rust.ts +103 -229
  132. package/src/server/card.tsx +65 -0
  133. package/src/server/compression.ts +115 -0
  134. package/src/server/core.ts +70 -0
  135. package/src/server/dispatch.ts +70 -0
  136. package/src/server/form.ts +85 -0
  137. package/src/server/functions.ts +129 -0
  138. package/src/server/handler.ts +39 -0
  139. package/src/server/head.ts +96 -0
  140. package/src/server/media.ts +60 -0
  141. package/src/server/navigation-cache.ts +80 -0
  142. package/src/server/navigation.tsx +85 -0
  143. package/src/server/page-cold.ts +95 -0
  144. package/src/server/page-stream.tsx +181 -0
  145. package/src/server/page-types.ts +51 -0
  146. package/src/server/page-warm.ts +130 -0
  147. package/src/server/page.ts +69 -0
  148. package/src/server/payload.ts +89 -0
  149. package/src/server/prerender.ts +60 -0
  150. package/src/server/preview.tsx +67 -0
  151. package/src/server/render.ts +167 -0
  152. package/src/server/request.ts +98 -0
  153. package/src/server/resolver.ts +228 -0
  154. package/src/server/routes.ts +150 -0
  155. package/src/server/services.ts +69 -0
  156. package/src/server/text.ts +29 -0
  157. package/src/server/types.ts +160 -0
  158. package/src/server.tsx +82 -971
  159. package/src/shape.mjs +30 -6
  160. package/src/share/display.ts +50 -0
  161. package/src/share/network.ts +119 -0
  162. package/src/share/receive.ts +70 -0
  163. package/src/share/state.ts +30 -0
  164. package/src/share/tab-queue.ts +28 -0
  165. package/src/share-server.ts +128 -43
  166. package/src/share.ts +103 -209
  167. package/src/sheets.mjs +30 -18
  168. package/src/sidecar/build.mjs +170 -0
  169. package/src/sidecar/cargo.mjs +164 -0
  170. package/src/sidecar/common.mjs +17 -0
  171. package/src/sidecar/dev-build.mjs +91 -0
  172. package/src/sidecar/dev-watch.mjs +88 -0
  173. package/src/sidecar/manifest.mjs +19 -0
  174. package/src/sidecar/process.mjs +58 -0
  175. package/src/sidecar/refresh.mjs +19 -0
  176. package/src/sidecar.mjs +75 -416
  177. package/src/socket/check.ts +50 -0
  178. package/src/socket/deliver.ts +16 -0
  179. package/src/socket/publish.ts +56 -0
  180. package/src/socket/rooms.ts +41 -0
  181. package/src/socket/route.ts +79 -0
  182. package/src/socket/shared.ts +26 -0
  183. package/src/socket/types.ts +48 -0
  184. package/src/socket.ts +205 -194
  185. package/src/stream.ts +10 -1
  186. package/src/verify/app.ts +39 -0
  187. package/src/verify/cleanup.ts +27 -0
  188. package/src/verify/database.ts +11 -0
  189. package/src/verify/live.ts +63 -0
  190. package/src/verify/notify.ts +35 -0
  191. package/src/verify/skip.ts +19 -0
  192. package/src/verify/support.ts +149 -0
  193. package/src/verify/timing.ts +15 -0
  194. package/src/verify/trigger.ts +14 -0
  195. package/src/verify.ts +77 -176
  196. package/src/view.tsx +96 -11
  197. package/src/vite/asset-bytes.mjs +95 -0
  198. package/src/vite/assets.mjs +117 -0
  199. package/src/vite/config-hooks.mjs +166 -0
  200. package/src/vite/dev-hooks.mjs +103 -0
  201. package/src/vite/dev.mjs +102 -0
  202. package/src/vite/entries.mjs +145 -0
  203. package/src/vite/module-hooks.mjs +288 -0
  204. package/src/vite/route-modules.mjs +289 -0
  205. package/src/vite/server-functions.mjs +222 -0
  206. package/src/vite/shape-entry-css.mjs +187 -0
  207. package/src/vite/shape-override.mjs +226 -0
  208. package/src/vite/shared.mjs +51 -0
  209. package/src/vite/sheets.mjs +92 -0
  210. package/src/vite/state.mjs +89 -0
  211. package/src/vite/syntax.mjs +139 -0
  212. package/src/vite.mjs +152 -853
  213. 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.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
- "type": "module",
7
- "dependencies": {
8
- "@fontsource/inter": "5.3.0",
9
- "@takumi-rs/core": "2.18.2",
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
- "peerDependencies": {
18
- "better-auth": "^1.7.0",
19
- "drizzle-orm": "^0.45.0",
20
- "drizzle-zod": "^0.8.0",
21
- "mysql2": "^3.24.0",
22
- "postgres": ">=3",
23
- "@weft-js/core": "^0.1.1",
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
- "sideEffects": [
65
- "**/*.css",
66
- "./src/share.ts"
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
- "keywords": [
101
- "weft"
102
- ],
103
- "files": [
104
- "src",
105
- "crate",
106
- "README.md",
107
- "LICENSE"
108
- ]
109
- }
117
+ }
118
+ }
package/src/app.mjs CHANGED
@@ -15,21 +15,39 @@ import weftStart, { startDir } from './vite.mjs'
15
15
  function usesSockets(root) {
16
16
  const src = join(root, 'src')
17
17
  if (!existsSync(src)) return false
18
- return readdirSync(src, { recursive: true, withFileTypes: true }).some(f => f.isFile() && /\.[cm]?[jt]sx?$/.test(f.name) && /\b(channel|sharedState)\s*[(<]/.test(readFileSync(join(f.parentPath, f.name), 'utf8')))
18
+ return readdirSync(src, { recursive: true, withFileTypes: true }).some(
19
+ (f) =>
20
+ f.isFile() &&
21
+ /\.[cm]?[jt]sx?$/.test(f.name) &&
22
+ /\b(channel|sharedState)\s*[(<]/.test(readFileSync(join(f.parentPath, f.name), 'utf8')),
23
+ )
19
24
  }
20
25
 
21
- function config({ root, site, hydration, preset, port, logLevel, command, nitro: nitroOptions = {} }) {
26
+ function config({
27
+ root,
28
+ site,
29
+ hydration,
30
+ preset,
31
+ port,
32
+ logLevel,
33
+ command,
34
+ nitro: nitroOptions = {},
35
+ }) {
22
36
  root = resolve(root)
23
37
  const sockets = usesSockets(root)
24
38
  const runner = devRunner(sockets)
25
- const user = ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs'].map(f => join(root, f)).find(existsSync)
39
+ const user = ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']
40
+ .map((f) => join(root, f))
41
+ .find(existsSync)
26
42
  return {
27
43
  root,
28
44
  configFile: user ?? false,
29
45
  logLevel,
30
46
  server: { port: port ?? 5173, strictPort: port !== undefined },
31
47
  // A function host can't hold a socket: the page goes straight to SSE, and a channel says `unavailable` (R024)
32
- ...(command === 'build' && { define: { __WEFT_SOCKETS__: JSON.stringify(holdsSockets(preset)) } }),
48
+ ...(command === 'build' && {
49
+ define: { __WEFT_SOCKETS__: JSON.stringify(holdsSockets(preset)) },
50
+ }),
33
51
  plugins: [
34
52
  weft({ hydratable: true }),
35
53
  weftStart({ root, site, hydration }),
@@ -38,7 +56,11 @@ function config({ root, site, hydration, preset, port, logLevel, command, nitro:
38
56
  preset,
39
57
  // Every app gets this: built assets ship as .br and .gz next to the file, and Nitro picks by Accept-Encoding.
40
58
  compressPublicAssets: { gzip: true, brotli: true },
41
- ...(site?.headers && { routeRules: Object.fromEntries(Object.entries(site.headers).map(([path, headers]) => [path, { headers }])) }),
59
+ ...(site?.headers && {
60
+ routeRules: Object.fromEntries(
61
+ Object.entries(site.headers).map(([path, headers]) => [path, { headers }]),
62
+ ),
63
+ }),
42
64
  ...(sockets && { features: { websocket: true } }),
43
65
  ...(runner && { devServer: { runner } }),
44
66
  // Build mode for share cards (research 20 §4.3): the server lists them and Nitro's prerender writes the PNGs.
@@ -46,9 +68,14 @@ function config({ root, site, hydration, preset, port, logLevel, command, nitro:
46
68
  // Prerendered cards are static files at content-addressed URLs: immutable. The rule names each file (a
47
69
  // `/_weft/og/**` rule would also override the handler's no-cache for a stale `?v=` on demand). Prerender runs
48
70
  // before Nitro bundles the server, so rules added when it is done are in the build. (Dev sends no-store.)
49
- ...(command === 'build' && { hooks: immutableCards(), plugins: [fileURLToPath(new URL('./range.mjs', import.meta.url))] }),
71
+ ...(command === 'build' && {
72
+ hooks: immutableCards(),
73
+ plugins: [fileURLToPath(new URL('./range.mjs', import.meta.url))],
74
+ }),
50
75
  // The media route: sharp's addon, and css-tree and csso (under svgo) read JSON from their packages at runtime.
51
- ...(existsSync(join(root, '.weft/slots/media.server.ts')) && { traceDeps: ['css-tree*', 'csso*', 'sharp*'] }),
76
+ ...(existsSync(join(root, '.weft/slots/media.server.ts')) && {
77
+ traceDeps: ['css-tree*', 'csso*', 'sharp*'],
78
+ }),
52
79
  ...nitroOptions,
53
80
  }),
54
81
  ],
@@ -69,7 +96,10 @@ function immutableCards() {
69
96
  'prerender:done'({ prerenderedRoutes }) {
70
97
  if (!built) return
71
98
  for (const r of prerenderedRoutes) {
72
- if (r.route.startsWith('/_weft/og/') && r.route.endsWith('.png')) built.options.routeRules[r.route] = { headers: { 'cache-control': 'public, max-age=31536000, immutable' } }
99
+ if (r.route.startsWith('/_weft/og/') && r.route.endsWith('.png'))
100
+ built.options.routeRules[r.route] = {
101
+ headers: { 'cache-control': 'public, max-age=31536000, immutable' },
102
+ }
73
103
  }
74
104
  built.routing.sync()
75
105
  },
@@ -0,0 +1,13 @@
1
+ import type { AssetReceipt, RouteAssetCosts } from './router'
2
+
3
+ /** Select only matched levels and union their emitted files for this build. */
4
+ export function selectRouteAssets(
5
+ receipt: AssetReceipt | null | undefined,
6
+ build: string | undefined,
7
+ ids: string[],
8
+ ): RouteAssetCosts | null
9
+ /** The costs, if they were measured for this build. */
10
+ export function matchingRouteAssets(
11
+ costs: RouteAssetCosts | undefined,
12
+ build: string | null | undefined,
13
+ ): RouteAssetCosts | null