@weft-js/start 0.1.1 → 0.1.3

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 (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. package/src/capture.d.mts +0 -1
@@ -0,0 +1,367 @@
1
+ //! `weft::build::generate("search")` in a sidecar's or an addon's `build.rs`: `.weft/contract.json` → serde types (typify), a typed
2
+ //! error enum per function, the `Service` trait and `functions()`. Rust never defines a schema (PLAN §4.3). A contract
3
+ //! that changes reruns this, and cargo then stops exactly where the implementation no longer fits, which `weft dev`,
4
+ //! `weft build` and `weft check` report as R103 with the function's id.
5
+
6
+ mod emit;
7
+
8
+ use std::collections::BTreeMap;
9
+ use std::env;
10
+ use std::fs;
11
+ use std::path::{Path, PathBuf};
12
+
13
+ use schemars::schema::{RootSchema, Schema};
14
+ use serde_json::{Map, Value, json};
15
+ use typify::{TypeSpace, TypeSpaceSettings};
16
+
17
+ /// Generate the contract for `service` into `OUT_DIR/weft_contract.rs`, and a copy to read at `.weft/rust/<service>.rs`.
18
+ ///
19
+ /// # Panics
20
+ /// When there's no contract or `service` has no functions. A build script reports failure by panicking; the message is
21
+ /// R103 with the fix.
22
+ pub fn generate(service: &str) {
23
+ println!("cargo:rerun-if-env-changed=WEFT_CONTRACT");
24
+ let path = contract_path().unwrap_or_else(|| panic!("R103: no .weft/contract.json above this crate. Run `weft dev` or `weft check` in the app, or set WEFT_CONTRACT"));
25
+ println!("cargo:rerun-if-changed={}", path.display());
26
+ let text = fs::read_to_string(&path)
27
+ .unwrap_or_else(|e| panic!("R103: can't read {}: {e}", path.display()));
28
+ let contract: Value = serde_json::from_str(&text)
29
+ .unwrap_or_else(|e| panic!("R103: {} isn't JSON: {e}", path.display()));
30
+ let code = render(&contract, service).unwrap_or_else(|e| panic!("R103: {e}"));
31
+ // An addon's undefined `napi_*` symbols are the host's, which macOS's linker has to be told.
32
+ if contract["services"][service]["kind"] == "native"
33
+ && env::var("CARGO_CFG_TARGET_FAMILY").is_ok_and(|f| !f.contains("wasm"))
34
+ {
35
+ napi_build::setup();
36
+ }
37
+ let out =
38
+ PathBuf::from(env::var("OUT_DIR").expect("cargo sets OUT_DIR")).join("weft_contract.rs");
39
+ fs::write(&out, &code).expect("OUT_DIR is writable");
40
+ if let Some(dir) = path.parent().map(|p| p.join("rust")) {
41
+ let _ = fs::create_dir_all(&dir);
42
+ let _ = fs::write(dir.join(format!("{service}.rs")), &code); // to read: a build doesn't depend on it
43
+ }
44
+ }
45
+
46
+ /// `WEFT_CONTRACT`, or the nearest `.weft/contract.json` above the crate.
47
+ fn contract_path() -> Option<PathBuf> {
48
+ if let Ok(p) = env::var("WEFT_CONTRACT") {
49
+ return Some(PathBuf::from(p));
50
+ }
51
+ let dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").ok()?);
52
+ dir.ancestors()
53
+ .map(|d: &Path| d.join(".weft/contract.json"))
54
+ .find(|p| p.exists())
55
+ }
56
+
57
+ /// `searchDocs` → `search_docs`, kept clear of what a method can't be called.
58
+ fn snake(name: &str) -> String {
59
+ let mut out = String::new();
60
+ for (i, c) in name.chars().enumerate() {
61
+ if c.is_ascii_uppercase() {
62
+ if i > 0 {
63
+ out.push('_');
64
+ }
65
+ out.push(c.to_ascii_lowercase());
66
+ } else if c.is_alphanumeric() || c == '_' {
67
+ out.push(c);
68
+ }
69
+ }
70
+ if syn::parse_str::<syn::Ident>(&out).is_err() {
71
+ format!("{out}_")
72
+ } else {
73
+ out
74
+ }
75
+ }
76
+
77
+ /// `searchDocs` → `SearchDocs`.
78
+ fn pascal(name: &str) -> String {
79
+ let clean: String = name
80
+ .chars()
81
+ .filter(|c| c.is_alphanumeric() || *c == '_')
82
+ .collect();
83
+ let mut chars = clean.chars();
84
+ chars
85
+ .next()
86
+ .map_or(String::new(), |c| c.to_uppercase().chain(chars).collect())
87
+ }
88
+
89
+ /// typify reads schemars 0.8 schemas, which spell a tuple `items: […]`. Zod's `$defs` are collected by the caller.
90
+ fn normal(v: &Value, defs: &mut BTreeMap<String, Value>) -> Value {
91
+ match v {
92
+ Value::Object(m) => {
93
+ let mut out = Map::new();
94
+ for (k, x) in m {
95
+ match k.as_str() {
96
+ "$schema" => {}
97
+ "$defs" | "definitions" => {
98
+ for (name, def) in x.as_object().into_iter().flatten() {
99
+ let def = normal(def, defs);
100
+ defs.insert(name.clone(), def);
101
+ }
102
+ }
103
+ "prefixItems" => {
104
+ out.insert("items".into(), normal(x, defs));
105
+ }
106
+ "$ref" => {
107
+ out.insert(
108
+ k.clone(),
109
+ json!(
110
+ x.as_str()
111
+ .unwrap_or("")
112
+ .replace("#/$defs/", "#/definitions/")
113
+ ),
114
+ );
115
+ }
116
+ _ => {
117
+ out.insert(k.clone(), normal(x, defs));
118
+ }
119
+ }
120
+ }
121
+ Value::Object(out)
122
+ }
123
+ Value::Array(a) => Value::Array(a.iter().map(|x| normal(x, defs)).collect()),
124
+ x => x.clone(),
125
+ }
126
+ }
127
+
128
+ struct Spec<'a> {
129
+ id: &'a str,
130
+ method: String,
131
+ type_name: String,
132
+ ctx: &'static str,
133
+ input: Option<Value>,
134
+ output: Value,
135
+ errors: Vec<(String, Value)>,
136
+ }
137
+
138
+ fn specs<'a>(functions: &[&'a Value], defs: &mut BTreeMap<String, Value>) -> Vec<Spec<'a>> {
139
+ let mut seen = BTreeMap::<String, usize>::new();
140
+ for f in functions {
141
+ *seen
142
+ .entry(f["name"].as_str().unwrap_or("").to_string())
143
+ .or_default() += 1;
144
+ }
145
+ functions
146
+ .iter()
147
+ .map(|f| {
148
+ let id = f["id"].as_str().unwrap_or("");
149
+ let name = f["name"].as_str().unwrap_or(id);
150
+ // Two files can export one name: the id's hash keeps them apart.
151
+ let unique = if seen[name] > 1 {
152
+ id.replace('.', "_")
153
+ } else {
154
+ name.to_string()
155
+ };
156
+ let imp = &f["impl"];
157
+ Spec {
158
+ id,
159
+ method: snake(&unique),
160
+ type_name: pascal(&unique),
161
+ ctx: match imp["ctx"].as_str() {
162
+ Some("shared") => "SharedCtx",
163
+ Some("user") => "UserCtx",
164
+ _ => "Ctx",
165
+ },
166
+ input: (!f["input"].is_null()).then(|| normal(&f["input"], defs)),
167
+ output: normal(&imp["output"], defs),
168
+ errors: imp["errors"]
169
+ .as_object()
170
+ .into_iter()
171
+ .flatten()
172
+ .map(|(k, v)| (k.clone(), normal(v, defs)))
173
+ .collect(),
174
+ }
175
+ })
176
+ .collect()
177
+ }
178
+
179
+ fn add(space: &mut TypeSpace, schema: &Value, name: &str) -> Result<String, String> {
180
+ let root: RootSchema = serde_json::from_value(schema.clone())
181
+ .map_err(|e| format!("`{name}` isn't a schema typify reads: {e}"))?;
182
+ let id = space
183
+ .add_type_with_name(&Schema::Object(root.schema), Some(name.to_string()))
184
+ .map_err(|e| format!("`{name}`: {e}"))?;
185
+ Ok(space
186
+ .get_type(&id)
187
+ .map_err(|e| e.to_string())?
188
+ .ident()
189
+ .to_string())
190
+ }
191
+
192
+ /// `pub type X = …` unless typify already named the type `X`.
193
+ fn alias(name: &str, ident: &str) -> String {
194
+ if ident.replace(' ', "") == name {
195
+ String::new()
196
+ } else {
197
+ format!("pub type {name} = {ident};\n")
198
+ }
199
+ }
200
+
201
+ /// The Rust for `service`'s functions.
202
+ pub(crate) fn render(contract: &Value, service: &str) -> Result<String, String> {
203
+ let all = contract["functions"]
204
+ .as_array()
205
+ .ok_or("the contract has no `functions`")?;
206
+ let functions: Vec<&Value> = all
207
+ .iter()
208
+ .filter(|f| f["impl"]["service"] == service)
209
+ .collect();
210
+ if functions.is_empty() {
211
+ return Err(format!(
212
+ "no function in the contract has `impl: remote('rust://{service}')` or `impl: native('{service}')`. Point one at it, or rename the service in build.rs"
213
+ ));
214
+ }
215
+ let hash = contract["services"][service]["hash"].as_str().unwrap_or("");
216
+
217
+ let mut defs = BTreeMap::new();
218
+ let specs = specs(&functions, &mut defs);
219
+ let mut settings = TypeSpaceSettings::default();
220
+ settings.with_struct_builder(false);
221
+ let mut space = TypeSpace::new(&settings);
222
+ let mut extra = String::new();
223
+ let mut body = String::new();
224
+
225
+ let refs = defs
226
+ .into_iter()
227
+ .map(|(k, v)| {
228
+ serde_json::from_value::<Schema>(v)
229
+ .map(|s| (k.clone(), s))
230
+ .map_err(|e| format!("`$defs/{k}`: {e}"))
231
+ })
232
+ .collect::<Result<Vec<_>, _>>()?;
233
+ space
234
+ .add_ref_types(refs)
235
+ .map_err(|e| format!("named schemas: {e}"))?;
236
+
237
+ let mut methods = String::new();
238
+ let mut table = String::new();
239
+ for s in &specs {
240
+ emit::function(
241
+ s,
242
+ &mut space,
243
+ &mut extra,
244
+ &mut body,
245
+ &mut methods,
246
+ &mut table,
247
+ )?;
248
+ }
249
+
250
+ let text = format!(
251
+ r"{types}
252
+ {extra}
253
+ {body}
254
+ /// The contract's hash: `GET /_weft/health` reports it, and neither side runs a binary built from another one.
255
+ pub const CONTRACT_HASH: &str = {hash:?};
256
+
257
+ /// Every function `{service}` implements. Written by `weft::build`: edit the TypeScript, not this.
258
+ pub trait Service: Send + Sync + 'static {{
259
+ {methods}
260
+ }}
261
+
262
+ /// What `weft::router` serves, or `weft::native!` exports.
263
+ pub fn functions<S: Service>(service: S) -> ::weft::Functions {{
264
+ let service = ::std::sync::Arc::new(service);
265
+ ::weft::Functions::new({service:?}, CONTRACT_HASH)
266
+ {table}
267
+ }}
268
+ ",
269
+ types = space.to_stream(),
270
+ );
271
+ let file = syn::parse_file(&text).map_err(|e| format!("generated code didn't parse: {e}"))?;
272
+ let pretty = prettyplease::unparse(&file);
273
+ Ok(pretty
274
+ .replace("::chrono::", "::weft::__rt::chrono::")
275
+ .replace("::uuid::", "::weft::__rt::uuid::")
276
+ .replace("::regress::", "::weft::__rt::regress::"))
277
+ }
278
+
279
+ #[cfg(test)]
280
+ #[allow(
281
+ clippy::needless_pass_by_value,
282
+ reason = "fixtures take `json!` literals"
283
+ )]
284
+ mod tests {
285
+ use super::*;
286
+
287
+ fn contract(functions: Value) -> Value {
288
+ json!({ "functions": functions, "services": { "search": { "hash": "h1" } } })
289
+ }
290
+
291
+ fn function(name: &str, input: Value, output: Value, ctx: &str, errors: Value) -> Value {
292
+ json!({ "id": format!("{name}.abc123"), "name": name, "input": input, "impl": { "service": "search", "ctx": ctx, "output": output, "errors": errors } })
293
+ }
294
+
295
+ #[test]
296
+ fn names_follow_the_function() {
297
+ assert_eq!(snake("searchDocs"), "search_docs");
298
+ assert_eq!(snake("match"), "match_");
299
+ assert_eq!(pascal("searchDocs"), "SearchDocs");
300
+ }
301
+
302
+ #[test]
303
+ fn one_method_a_function_with_its_context_and_errors() {
304
+ let code = render(
305
+ &contract(json!([
306
+ function("searchDocs", json!({ "type": "object", "properties": { "q": { "type": "string" } }, "required": ["q"] }), json!({ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } }), "shared", json!({})),
307
+ function("saveDoc", Value::Null, json!({ "type": "null" }), "user", json!({ "NotFound": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } })),
308
+ function("other", Value::Null, json!({ "type": "string" }), "ctx", json!({})),
309
+ ])),
310
+ "search",
311
+ )
312
+ .unwrap();
313
+ assert!(code.contains("fn search_docs("), "{code}");
314
+ assert!(code.contains("ctx: ::weft::SharedCtx"));
315
+ assert!(code.contains("input: SearchDocsInput"));
316
+ assert!(code.contains("pub type SearchDocsOutput ="));
317
+ assert!(code.contains("ctx: ::weft::UserCtx"));
318
+ assert!(!code.contains("SaveDocInput"), "no input, no parameter");
319
+ assert!(code.contains("pub enum SaveDocError"));
320
+ assert!(code.contains("NotFound(SaveDocNotFound)"));
321
+ assert!(code.contains("\"h1\""));
322
+ }
323
+
324
+ #[test]
325
+ fn a_service_with_no_functions_says_how_to_fix_it() {
326
+ let e = render(&contract(json!([])), "search").unwrap_err();
327
+ assert!(e.contains("rust://search"), "{e}");
328
+ }
329
+
330
+ #[test]
331
+ fn a_tuple_and_a_named_schema_from_zod() {
332
+ let output = json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", "items": { "$ref": "#/$defs/Hit" }, "$defs": { "Hit": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } } });
333
+ let tuple = json!({ "type": "array", "prefixItems": [{ "type": "string" }, { "type": "number" }], "minItems": 2, "maxItems": 2 });
334
+ let code = render(
335
+ &contract(json!([function("hits", tuple, output, "ctx", json!({}))])),
336
+ "search",
337
+ )
338
+ .unwrap();
339
+ assert!(code.contains("pub struct Hit"), "{code}");
340
+ assert!(
341
+ code.contains("pub type HitsOutput = ::std::vec::Vec<Hit>"),
342
+ "{code}"
343
+ );
344
+ assert!(
345
+ code.contains("pub type HitsInput = (::std::string::String, f64)"),
346
+ "{code}"
347
+ );
348
+ }
349
+
350
+ #[test]
351
+ fn dates_and_uuids_need_no_dependency_of_the_app() {
352
+ let input = json!({ "type": "object", "properties": { "at": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" } }, "required": ["at", "id"] });
353
+ let code = render(
354
+ &contract(json!([function(
355
+ "f",
356
+ input,
357
+ json!({ "type": "null" }),
358
+ "ctx",
359
+ json!({})
360
+ )])),
361
+ "search",
362
+ )
363
+ .unwrap();
364
+ assert!(code.contains("::weft::__rt::chrono::"), "{code}");
365
+ assert!(code.contains("::weft::__rt::uuid::"), "{code}");
366
+ }
367
+ }
@@ -0,0 +1,186 @@
1
+ //! What a handler knows about its call. There are three, and the generated trait picks one per function from its
2
+ //! declaration, so what TS guarantees is a compile error in Rust rather than a convention:
3
+ //!
4
+ //! - [`Ctx`]: `ctx.user()` is an `Option`, as `ctx.user` is nullable in TS.
5
+ //! - [`UserCtx`] (`auth: 'user'`): `ctx.user()` is the user; TS refused the call otherwise.
6
+ //! - [`SharedCtx`] (`cache: { life }`): there is no `user()` at all, because the value is shared by every user.
7
+ //!
8
+ //! ```compile_fail
9
+ //! fn leaks(ctx: weft::SharedCtx) { let _ = ctx.user(); }
10
+ //! ```
11
+
12
+ use std::ops::Deref;
13
+ use std::sync::{Arc, Mutex};
14
+ use std::time::Duration;
15
+
16
+ use serde::Deserialize;
17
+
18
+ /// The claims TS sends: never the session, and only what `remote({ claims })` chose to send.
19
+ #[derive(Debug, Clone, Deserialize)]
20
+ pub struct User {
21
+ pub id: String,
22
+ #[serde(default)]
23
+ pub roles: Vec<String>,
24
+ #[serde(default)]
25
+ pub tenant: Option<String>,
26
+ /// Anything else a `claims` function returned.
27
+ #[serde(flatten)]
28
+ pub extra: serde_json::Map<String, serde_json::Value>,
29
+ }
30
+
31
+ impl User {
32
+ pub fn has_role(&self, role: &str) -> bool {
33
+ self.roles.iter().any(|r| r == role)
34
+ }
35
+ }
36
+
37
+ /// What TS tells a sidecar or an addon about the call. Claims only: never a cookie or a session token.
38
+ #[derive(Debug, Clone, Deserialize)]
39
+ #[serde(rename_all = "camelCase")]
40
+ pub struct Envelope {
41
+ pub user: Option<User>,
42
+ pub request_id: String,
43
+ pub deadline_ms: u64,
44
+ pub traceparent: Option<String>,
45
+ /// Milliseconds since the epoch when TS signed it. Nothing signs an in-process call.
46
+ #[serde(default)]
47
+ pub iat: u64,
48
+ }
49
+
50
+ /// A monotonic clock. `Instant::now()` panics on wasm32-unknown-unknown, so there the host tells the time.
51
+ #[cfg(not(target_family = "wasm"))]
52
+ fn clock() -> Duration {
53
+ static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
54
+ START.get_or_init(std::time::Instant::now).elapsed()
55
+ }
56
+
57
+ #[cfg(target_family = "wasm")]
58
+ fn clock() -> Duration {
59
+ Duration::from_secs_f64(crate::native::now_ms() / 1000.0)
60
+ }
61
+
62
+ #[derive(Debug)]
63
+ struct Inner {
64
+ function: String,
65
+ user: Option<User>,
66
+ request_id: String,
67
+ traceparent: Option<String>,
68
+ deadline: Duration,
69
+ tags: Mutex<Vec<String>>,
70
+ touched: Mutex<Vec<String>>,
71
+ }
72
+
73
+ /// What every context has: the deadline, the ids and the two reports back to TS.
74
+ #[derive(Debug, Clone)]
75
+ pub struct Base(Arc<Inner>);
76
+
77
+ impl Base {
78
+ pub(crate) fn new(function: &str, envelope: Envelope) -> Self {
79
+ Base(Arc::new(Inner {
80
+ function: function.to_string(),
81
+ user: envelope.user,
82
+ request_id: envelope.request_id,
83
+ traceparent: envelope.traceparent,
84
+ deadline: clock() + Duration::from_millis(envelope.deadline_ms),
85
+ tags: Mutex::default(),
86
+ touched: Mutex::default(),
87
+ }))
88
+ }
89
+
90
+ /// Report a table this call read (a query) or wrote (a mutation). `weft dev` checks it against the function's tags
91
+ /// (Q011), as it does for a TS `run`; it can't see into Rust any other way.
92
+ pub fn touch(&self, table: impl Into<String>) {
93
+ self.0.touched.lock().expect("touched").push(table.into());
94
+ }
95
+
96
+ /// Add a cache tag to this call's result, or, in a mutation, a tag to invalidate. Declared tags stay in TS: Rust can
97
+ /// add to them and never remove one.
98
+ pub fn tag(&self, tag: impl Into<String>) {
99
+ self.0.tags.lock().expect("tags").push(tag.into());
100
+ }
101
+
102
+ /// The function's id, as the contract spells it.
103
+ pub fn function(&self) -> &str {
104
+ &self.0.function
105
+ }
106
+
107
+ /// The id TS gave this request. Log it: the client sees it on an `Internal` failure.
108
+ pub fn request_id(&self) -> &str {
109
+ &self.0.request_id
110
+ }
111
+
112
+ pub fn traceparent(&self) -> Option<&str> {
113
+ self.0.traceparent.as_deref()
114
+ }
115
+
116
+ /// What's left of the deadline. weft stops waiting at zero; long CPU loops should check [`Base::cancelled`].
117
+ pub fn remaining(&self) -> Duration {
118
+ self.0.deadline.saturating_sub(clock())
119
+ }
120
+
121
+ /// The deadline passed, so no one is waiting for this answer.
122
+ pub fn cancelled(&self) -> bool {
123
+ self.remaining().is_zero()
124
+ }
125
+
126
+ pub(crate) fn tags(&self) -> Vec<String> {
127
+ self.0.tags.lock().expect("tags").clone()
128
+ }
129
+
130
+ pub(crate) fn touched(&self) -> Vec<String> {
131
+ self.0.touched.lock().expect("touched").clone()
132
+ }
133
+
134
+ pub(crate) fn user(&self) -> Option<&User> {
135
+ self.0.user.as_ref()
136
+ }
137
+ }
138
+
139
+ /// How the generated code turns the common [`Base`] into the context a function's declaration calls for. `None` means
140
+ /// the claims don't fit, which only a wrong signer can cause.
141
+ pub trait FromBase: Sized {
142
+ fn from_base(base: Base) -> Option<Self>;
143
+ }
144
+
145
+ /// A public function: `user()` is an `Option`.
146
+ #[derive(Debug, Clone)]
147
+ pub struct Ctx(Base);
148
+
149
+ impl Ctx {
150
+ pub fn user(&self) -> Option<&User> {
151
+ self.0.user()
152
+ }
153
+ }
154
+
155
+ /// `auth: 'user'`: TS refused the call unless someone is signed in.
156
+ #[derive(Debug, Clone)]
157
+ pub struct UserCtx(Base);
158
+
159
+ impl UserCtx {
160
+ pub fn user(&self) -> &User {
161
+ self.0.user().expect("from_base checked it")
162
+ }
163
+ }
164
+
165
+ /// `cache: { life }`: one value for every user, so no user. Take what the value depends on as input.
166
+ #[derive(Debug, Clone)]
167
+ pub struct SharedCtx(Base);
168
+
169
+ macro_rules! context {
170
+ ($($name:ident: $check:expr),*) => {$(
171
+ impl Deref for $name {
172
+ type Target = Base;
173
+ fn deref(&self) -> &Base {
174
+ &self.0
175
+ }
176
+ }
177
+ impl FromBase for $name {
178
+ fn from_base(base: Base) -> Option<Self> {
179
+ let fits: fn(&Base) -> bool = $check;
180
+ fits(&base).then_some(Self(base))
181
+ }
182
+ }
183
+ )*};
184
+ }
185
+
186
+ context!(Ctx: |_| true, UserCtx: |b| b.user().is_some(), SharedCtx: |b| b.user().is_none());