@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.
- package/README.md +1278 -12
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/README.md +322 -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 +38 -8
- 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 +120 -492
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +144 -157
- 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 +90 -166
- 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 +112 -37
- 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 -178
- 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 -971
- 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
package/crate/Cargo.toml
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "weft"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
description = "The Rust side of weft's remote() and native(): generated types from .weft/contract.json, Ctx, the signed envelope, an axum Router, a Node-API and WASM entry"
|
|
7
|
+
|
|
8
|
+
[features]
|
|
9
|
+
default = ["server"]
|
|
10
|
+
# What generated code needs on any target.
|
|
11
|
+
core = ["dep:chrono", "dep:regress", "dep:serde", "dep:serde_json", "dep:uuid"]
|
|
12
|
+
# What a sidecar links.
|
|
13
|
+
server = ["core", "dep:axum", "dep:base64", "dep:hmac", "dep:sha2", "dep:tokio", "dep:tracing", "dep:tracing-subscriber"]
|
|
14
|
+
# What a `native()` crate links, with `default-features = false`: an addon, or a .wasm on that target. No server, no tokio.
|
|
15
|
+
native = ["core", "dep:napi", "dep:napi-derive"]
|
|
16
|
+
# `span.set_parent` from the envelope's traceparent, for an app that installs an OpenTelemetry layer.
|
|
17
|
+
otel = ["server", "dep:opentelemetry", "dep:tracing-opentelemetry"]
|
|
18
|
+
# What a sidecar's build.rs links, with `default-features = false`: nothing of the server.
|
|
19
|
+
build = ["dep:napi-build", "dep:prettyplease", "dep:schemars", "dep:serde_json", "dep:syn", "dep:typify"]
|
|
20
|
+
|
|
21
|
+
[dependencies]
|
|
22
|
+
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "tokio"], optional = true }
|
|
23
|
+
base64 = { version = "0.23", optional = true }
|
|
24
|
+
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"], optional = true }
|
|
25
|
+
hmac = { version = "0.13", optional = true }
|
|
26
|
+
napi-build = { version = "2.4", optional = true }
|
|
27
|
+
opentelemetry = { version = "0.33", optional = true }
|
|
28
|
+
prettyplease = { version = "0.3", optional = true }
|
|
29
|
+
regress = { version = "0.11", optional = true }
|
|
30
|
+
schemars = { version = "0.8.22", optional = true } # the version typify reads
|
|
31
|
+
serde = { version = "1", features = ["derive"], optional = true }
|
|
32
|
+
serde_json = { version = "1", optional = true }
|
|
33
|
+
sha2 = { version = "0.11", optional = true }
|
|
34
|
+
syn = { version = "3", features = ["full"], optional = true }
|
|
35
|
+
tokio = { version = "1.54", features = ["macros", "net", "rt-multi-thread", "signal", "time"], optional = true }
|
|
36
|
+
tracing = { version = "0.1", optional = true }
|
|
37
|
+
tracing-opentelemetry = { version = "0.34", optional = true }
|
|
38
|
+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "env-filter", "fmt"], optional = true }
|
|
39
|
+
typify = { version = "0.8.0", optional = true }
|
|
40
|
+
uuid = { version = "1", features = ["serde"], optional = true }
|
|
41
|
+
|
|
42
|
+
# Not on wasm: there the crate exports a small ABI of its own (src/native.rs).
|
|
43
|
+
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
|
44
|
+
napi = { version = "3.12", default-features = false, features = ["dyn-symbols", "napi4"], optional = true }
|
|
45
|
+
napi-derive = { version = "3.6", optional = true }
|
|
46
|
+
|
|
47
|
+
[dev-dependencies]
|
|
48
|
+
tower = { version = "0.5", features = ["util"] }
|
|
49
|
+
|
|
50
|
+
[lib]
|
|
51
|
+
doctest = true
|
|
52
|
+
|
|
53
|
+
[lints.rust]
|
|
54
|
+
unsafe_op_in_unsafe_fn = "deny"
|
|
55
|
+
|
|
56
|
+
[lints.clippy]
|
|
57
|
+
pedantic = { level = "deny", priority = -1 }
|
|
58
|
+
unwrap_used = "deny"
|
|
59
|
+
undocumented_unsafe_blocks = "deny"
|
|
60
|
+
allow_attributes_without_reason = "deny"
|
|
61
|
+
# Pedantic lints that cost more than they catch here. Byte offsets are u32 by Oxc's design; the rest are taste.
|
|
62
|
+
cast_possible_truncation = "allow"
|
|
63
|
+
cast_possible_wrap = "allow"
|
|
64
|
+
cast_sign_loss = "allow"
|
|
65
|
+
cast_precision_loss = "allow"
|
|
66
|
+
module_name_repetitions = "allow"
|
|
67
|
+
must_use_candidate = "allow"
|
|
68
|
+
missing_errors_doc = "allow"
|
|
69
|
+
missing_panics_doc = "allow"
|
|
70
|
+
too_many_lines = "deny"
|
|
71
|
+
similar_names = "allow"
|
|
72
|
+
struct_excessive_bools = "allow"
|
package/crate/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/Ravonus/weft">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Ravonus/weft/main/site/public/logo.svg" width="88" height="88" alt="Weft">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<h1 align="center">weft (Rust crate)</h1>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<strong>The Rust side of Weft's <code>remote()</code> and <code>native()</code>: server functions declared in TypeScript, implemented in Rust.</strong><br>
|
|
11
|
+
Zod stays the source of truth. The crate generates the types, a <code>Service</code> trait and the transport, so a contract change stops <code>cargo</code> where the Rust no longer fits.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="../../LICENSE"><img src="https://img.shields.io/badge/license-MIT-145c3c" alt="MIT license"></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/rust-1.99.1-145c3c" alt="Rust toolchain 1.99.1">
|
|
17
|
+
<img src="https://img.shields.io/badge/edition-2024-145c3c" alt="Rust edition 2024">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="#quick-start">Quick start</a> ·
|
|
22
|
+
<a href="../../site/content/learn/rust.md">Rust guide</a> ·
|
|
23
|
+
<a href="../../packages/cli/docs/rust.md">Rust reference</a> ·
|
|
24
|
+
<a href="../README.md">All crates</a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> This crate is not on crates.io. `@weft-js/start` ships a copy of it in its package as `crate/` (written at `prepack`
|
|
29
|
+
> by `packages/start/scripts/crate.mjs`, without `target/` and `tests/`), and an app's `Cargo.toml` points there by
|
|
30
|
+
> path. That copy is always the version the installed TypeScript speaks. `weft add rust` writes the dependency for you.
|
|
31
|
+
|
|
32
|
+
A Weft server function can have a Rust body instead of a TypeScript `run`. You declare it in TypeScript with Zod, as
|
|
33
|
+
any other function, and give it `impl: <target>`. `weft check`, `weft dev` and `weft build` write the schemas to
|
|
34
|
+
`.weft/contract.json`, and this crate's `build` feature turns that file into Rust: a struct per input and output, a
|
|
35
|
+
typed error enum per function, and one `Service` trait. You implement the trait; the crate serves it as an HTTP sidecar
|
|
36
|
+
(`remote()`) or exports it in-process as a Node-API addon or a `.wasm` (`native()`).
|
|
37
|
+
|
|
38
|
+
TypeScript keeps the public edge: sessions, CSRF, the cache and validation on both sides of the call. Rust only sees
|
|
39
|
+
input that already passed, and claims, never cookies.
|
|
40
|
+
|
|
41
|
+
## Highlights
|
|
42
|
+
|
|
43
|
+
- **No hand-written Rust types.** `weft::build::generate("<service>")` reads `.weft/contract.json` and writes the types
|
|
44
|
+
with typify, the error enums, the `Service` trait and `functions()` into `OUT_DIR`.
|
|
45
|
+
- **Drift stops the build.** A contract that changes reruns the build script, and `cargo` fails exactly where the
|
|
46
|
+
implementation no longer fits. Weft reports it as `R103` with the function's id.
|
|
47
|
+
- **The context matches the declaration.** A function gets `Ctx`, `UserCtx` (`auth: 'user'`) or `SharedCtx` (a shared
|
|
48
|
+
cache), and `SharedCtx` has no `user()` at all, so leaking per-user data into a shared result is a compile error.
|
|
49
|
+
- **Signed calls.** A sidecar call carries `weft-ctx` and `weft-sig` headers: an HMAC-SHA256 over the function id, the
|
|
50
|
+
context and the body, with `iat` checked against a 60-second skew.
|
|
51
|
+
- **One reply shape on every transport.** `{ "ok": true, "value", "tags", "touched" }` or
|
|
52
|
+
`{ "ok": false, "error": { "code", … } }`, from HTTP, Node-API and WASM alike.
|
|
53
|
+
- **A panic is one failed call.** Each sidecar call runs in its own task with the deadline TypeScript sent; a panic or
|
|
54
|
+
an overrun answers that call and leaves the process serving.
|
|
55
|
+
- **Feature-gated.** A `native()` addon links no server and no tokio; a build script links only the generator.
|
|
56
|
+
|
|
57
|
+
## Contents
|
|
58
|
+
|
|
59
|
+
- [Quick start](#quick-start)
|
|
60
|
+
- [Features](#features)
|
|
61
|
+
- [The generated contract](#the-generated-contract)
|
|
62
|
+
- [Contexts](#contexts)
|
|
63
|
+
- [`remote()`: the sidecar](#remote-the-sidecar)
|
|
64
|
+
- [`native()`: in-process](#native-in-process)
|
|
65
|
+
- [Errors](#errors)
|
|
66
|
+
- [API reference](#api-reference)
|
|
67
|
+
- [Troubleshooting](#troubleshooting)
|
|
68
|
+
- [Build and test](#build-and-test)
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
In an app, let the CLI write the crate:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
weft add rust media # native(): an in-process addon
|
|
76
|
+
weft add rust search --remote # remote(): a sidecar
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For `remote()`, the crate depends on `weft` twice: the default features for the sidecar, and only `build` for the build
|
|
80
|
+
script. With the copy inside `@weft-js/start`, that looks like this:
|
|
81
|
+
|
|
82
|
+
```toml
|
|
83
|
+
[dependencies]
|
|
84
|
+
serde = { version = "1", features = ["derive"] }
|
|
85
|
+
serde_json = "1"
|
|
86
|
+
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|
87
|
+
weft = { path = "../../node_modules/@weft-js/start/crate" }
|
|
88
|
+
|
|
89
|
+
[build-dependencies]
|
|
90
|
+
weft = { path = "../../node_modules/@weft-js/start/crate", default-features = false, features = ["build"] }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```rust
|
|
94
|
+
// build.rs
|
|
95
|
+
fn main() {
|
|
96
|
+
weft::build::generate("search");
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```rust
|
|
101
|
+
// src/main.rs
|
|
102
|
+
weft::contract!();
|
|
103
|
+
use contract::*;
|
|
104
|
+
|
|
105
|
+
struct Search;
|
|
106
|
+
|
|
107
|
+
impl Service for Search {
|
|
108
|
+
async fn search_docs(
|
|
109
|
+
&self,
|
|
110
|
+
ctx: weft::SharedCtx,
|
|
111
|
+
input: SearchDocsInput,
|
|
112
|
+
) -> Result<SearchDocsOutput, weft::Error> {
|
|
113
|
+
ctx.touch("docs");
|
|
114
|
+
Ok(vec![])
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[tokio::main]
|
|
119
|
+
async fn main() -> std::io::Result<()> {
|
|
120
|
+
weft::serve(weft::router(contract::functions(Search))).await
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For `native()`, the crate is a `cdylib`, depends on `weft` with `default-features = false, features = ["native"]`, and
|
|
125
|
+
replaces `main` with one line:
|
|
126
|
+
|
|
127
|
+
```rust
|
|
128
|
+
weft::native!(contract::functions(Media));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The [Rust guide](../../site/content/learn/rust.md) walks through both, including the TypeScript side.
|
|
132
|
+
|
|
133
|
+
## Features
|
|
134
|
+
|
|
135
|
+
| Feature | Default | Enables | Use it for |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| `core` | via `server` | `chrono`, `regress`, `serde`, `serde_json`, `uuid`; the `Ctx` types, `Functions`, `Error` | What generated code needs on any target |
|
|
138
|
+
| `server` | yes | `core` plus `axum`, `tokio`, `hmac`, `sha2`, `base64`, `tracing`, `tracing-subscriber`; `router`, `router_with`, `serve`, `sign`, `verify` | A `remote()` sidecar |
|
|
139
|
+
| `native` | no | `core` plus `napi`, `napi-derive` (not on wasm); `weft::native!` | A `native()` crate, as an addon or a `.wasm`. No server, no tokio |
|
|
140
|
+
| `otel` | no | `server` plus `opentelemetry`, `tracing-opentelemetry` | Continue the trace TypeScript started, for an app that installs an OpenTelemetry layer |
|
|
141
|
+
| `build` | no | `typify`, `schemars`, `syn`, `prettyplease`, `napi-build`, `serde_json`; `weft::build::generate` | A sidecar's or addon's `build.rs` |
|
|
142
|
+
|
|
143
|
+
## The generated contract
|
|
144
|
+
|
|
145
|
+
`weft::build::generate(service)` does this, in a build script:
|
|
146
|
+
|
|
147
|
+
1. Finds the contract: `WEFT_CONTRACT` if set, else the nearest `.weft/contract.json` above the crate.
|
|
148
|
+
2. Turns each function of `service` into Rust. Names convert from camelCase: `searchDocs` becomes the method
|
|
149
|
+
`search_docs` and the types `SearchDocsInput`, `SearchDocsOutput`. Two files that export the same name are kept
|
|
150
|
+
apart by the function id.
|
|
151
|
+
3. Writes `OUT_DIR/weft_contract.rs`, and a copy to read at `.weft/rust/<service>.rs` (a build never depends on the
|
|
152
|
+
copy).
|
|
153
|
+
4. For a `native` service on a non-wasm target, runs `napi_build::setup()` so the linker accepts the host's `napi_*`
|
|
154
|
+
symbols.
|
|
155
|
+
|
|
156
|
+
`weft::contract!()` includes the generated file as `pub mod contract`, which holds:
|
|
157
|
+
|
|
158
|
+
| Item | What it is |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `<Name>Input`, `<Name>Output` | serde types for each function, from its Zod schemas |
|
|
161
|
+
| `<Name>Error` | A typed error enum, for each function that declares `errors` |
|
|
162
|
+
| `trait Service` | One `async fn` per function, each taking the context its declaration calls for |
|
|
163
|
+
| `fn functions(service) -> weft::Functions` | The dispatch table that `weft::router` serves and `weft::native!` exports |
|
|
164
|
+
| `const CONTRACT_HASH` | The contract's hash. The sidecar's health check reports it, and neither side runs a binary built from another one |
|
|
165
|
+
|
|
166
|
+
## Contexts
|
|
167
|
+
|
|
168
|
+
The generated trait picks one context per function, from its declaration in TypeScript:
|
|
169
|
+
|
|
170
|
+
| Type | When | `user()` |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `weft::Ctx` | default | `Option<&User>`, as `ctx.user` is nullable in TypeScript |
|
|
173
|
+
| `weft::UserCtx` | `auth: 'user'` | `&User`: TypeScript refused the call otherwise |
|
|
174
|
+
| `weft::SharedCtx` | a shared cache (`cache: { life }`) | none: the value is shared by every user |
|
|
175
|
+
|
|
176
|
+
All three dereference to `weft::Base`, which has:
|
|
177
|
+
|
|
178
|
+
| Method | What it does |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `touch(table)` | Report a table this call read or wrote. `weft dev` checks it against the function's tags (Q011) |
|
|
181
|
+
| `tag(tag)` | Add a cache tag to the result, or in a mutation a tag to invalidate. Rust can add tags, never remove declared ones |
|
|
182
|
+
| `function()` | The function's id, as the contract spells it |
|
|
183
|
+
| `request_id()` | The id TypeScript gave this request. The client sees it on an `Internal` failure, so log it |
|
|
184
|
+
| `traceparent()` | The W3C `traceparent` TypeScript sent, if any |
|
|
185
|
+
| `remaining()` | What's left of the deadline |
|
|
186
|
+
| `cancelled()` | `true` once the deadline has passed, so long CPU loops can stop |
|
|
187
|
+
|
|
188
|
+
`weft::User` carries `id`, `roles`, an optional `tenant`, and any other claims in `extra`; `has_role(role)` checks
|
|
189
|
+
`roles`. These are only what `remote({ claims })` chose to send.
|
|
190
|
+
|
|
191
|
+
## `remote()`: the sidecar
|
|
192
|
+
|
|
193
|
+
`weft::router(functions)` returns an axum `Router`, so a sidecar can be one binary or part of a larger axum app:
|
|
194
|
+
|
|
195
|
+
| Route | What it does |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `POST /_weft/fn/{id}` | Verify the signature, deserialize `{ "input": … }`, run the function in its own task under the deadline, answer |
|
|
198
|
+
| `GET /_weft/health` | `{ ok, service, contract, functions, pid }`, for the supervisor |
|
|
199
|
+
|
|
200
|
+
`router` reads its key from `WEFT_RUST_SECRET` and panics with `R106` when it is unset, because a sidecar that took
|
|
201
|
+
unsigned calls would trust any process that could reach it. `router_with(functions, secret)` takes the key directly.
|
|
202
|
+
|
|
203
|
+
`weft::serve(router)` listens on `WEFT_LISTEN` (`127.0.0.1:0` when unset), prints `weft-listening <addr>` for the
|
|
204
|
+
supervisor, logs to stderr by `RUST_LOG` (default `info`) unless the app installed a subscriber, and drains on SIGTERM
|
|
205
|
+
or Ctrl-C.
|
|
206
|
+
|
|
207
|
+
### The envelope
|
|
208
|
+
|
|
209
|
+
Each call carries two headers:
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
weft-ctx: base64url(JSON of the Envelope)
|
|
213
|
+
weft-sig: hex(HMAC-SHA256(secret, "weft1\n" + id + "\n" + weft-ctx + "\n" + body))
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The envelope is `{ user, requestId, deadlineMs, traceparent, iat }`. The signature covers the function id and the body
|
|
217
|
+
as well as the claims, so a signed call can't be replayed against another function or with another input, and `iat`
|
|
218
|
+
bounds how long it can be replayed at all (`MAX_SKEW_MS` is `60_000`). `weft::sign` and `weft::verify` are public, for
|
|
219
|
+
tests and other hosts.
|
|
220
|
+
|
|
221
|
+
### What a call answers
|
|
222
|
+
|
|
223
|
+
| Outcome | Status | Body |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| Value | 200 | `{ "ok": true, "value": …, "tags": […], "touched": […] }` |
|
|
226
|
+
| Declared error | 200 | `{ "ok": false, "error": { "code": …, "data": … } }` |
|
|
227
|
+
| Panic, `weft::Error`, or a result that doesn't serialize | 500 | `{ "ok": false, "error": { "code": "Internal", "requestId": … } }`; the detail stays in the sidecar's log |
|
|
228
|
+
| Input serde refuses, or claims that don't fit the context | 422 | `{ "ok": false, "error": { "code": "Drift", "message": … } }` |
|
|
229
|
+
| Unknown function id | 404 | `Drift`, naming the id |
|
|
230
|
+
| Missing, malformed or wrong signature, or stale `iat` | 401 | `Refused` |
|
|
231
|
+
| Deadline passed | 504 | `Deadline` |
|
|
232
|
+
|
|
233
|
+
When TypeScript hangs up, axum drops the handler's future and the crate aborts the task.
|
|
234
|
+
|
|
235
|
+
### Tracing
|
|
236
|
+
|
|
237
|
+
Each call runs in a `weft.fn` span with `function`, `request_id`, `trace_id` and `parent_id` fields, taken from the
|
|
238
|
+
envelope's `traceparent`. With the `otel` feature, the span's parent is set from `traceparent`, so the call continues
|
|
239
|
+
the trace TypeScript started.
|
|
240
|
+
|
|
241
|
+
## `native()`: in-process
|
|
242
|
+
|
|
243
|
+
`weft::native!(contract::functions(Svc))` exports the service from a `cdylib`. There is no HTTP and no signature,
|
|
244
|
+
because nothing outside the process can reach it. The service is built on the first call, not while the library loads.
|
|
245
|
+
|
|
246
|
+
| Target | Exports | How a call runs |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| Node-API addon | `call(id, ctx, body)` (async), `contract()` | On the host's worker pool, so a long call doesn't hold the event loop |
|
|
249
|
+
| `wasm32` | `weft_init`, `weft_alloc`, `weft_free`, `weft_call`, `weft_contract`; imports `weft.now` and `weft.panicked` | On whatever thread calls it. Returns `pointer << 32 | length` |
|
|
250
|
+
|
|
251
|
+
`contract()` returns `{ service, hash, functions }`, and TypeScript refuses a library built from another contract
|
|
252
|
+
(R103).
|
|
253
|
+
|
|
254
|
+
> [!WARNING]
|
|
255
|
+
> `native()` brings no async runtime. It is for CPU work. A function that waits on IO belongs in `remote()`: on the
|
|
256
|
+
> WASM build a method that doesn't complete without waiting panics.
|
|
257
|
+
|
|
258
|
+
## Errors
|
|
259
|
+
|
|
260
|
+
A function that declares no `errors` returns `weft::Error`. Any `std::error::Error` converts into it, so `?` works, and
|
|
261
|
+
`weft::Error::msg("…")` makes one from text. It always becomes an `Internal` failure: the client gets the request id
|
|
262
|
+
and the detail stays on the server.
|
|
263
|
+
|
|
264
|
+
A function that declares `errors` returns its generated `<Name>Error` enum, which implements `weft::Failure` and
|
|
265
|
+
serializes as `{ "code": …, "data": … }`. TypeScript checks it against the declaration, and the client catches
|
|
266
|
+
`e.code` and `e.data`.
|
|
267
|
+
|
|
268
|
+
## API reference
|
|
269
|
+
|
|
270
|
+
| Item | Feature | Description |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| `weft::contract!()` | any | Include the generated contract as `pub mod contract` |
|
|
273
|
+
| `weft::native!(functions)` | `native` | Export the functions as a Node-API addon, or as a `.wasm` on that target |
|
|
274
|
+
| `weft::build::generate(service)` | `build` | Generate the contract in a build script. Panics with `R103` and a fix when it can't |
|
|
275
|
+
| `weft::router(functions)` | `server` | An axum `Router` keyed by `WEFT_RUST_SECRET` |
|
|
276
|
+
| `weft::router_with(functions, secret)` | `server` | The same, with the key in hand |
|
|
277
|
+
| `weft::serve(router)` | `server` | Serve on `WEFT_LISTEN` and drain on SIGTERM or Ctrl-C |
|
|
278
|
+
| `weft::sign(secret, id, ctx, body)` | `server` | The `weft-sig` value for a call |
|
|
279
|
+
| `weft::verify(secret, id, ctx, sig, body, now_ms)` | `server` | Check a call and decode its `Envelope`, or say why it is `Refused` |
|
|
280
|
+
| `weft::Refused` | `server` | `Malformed`, `BadSignature` or `Stale` |
|
|
281
|
+
| `weft::Ctx`, `UserCtx`, `SharedCtx` | `core` | The three contexts |
|
|
282
|
+
| `weft::Base`, `FromBase` | `core` | What every context has, and how generated code builds a context from it |
|
|
283
|
+
| `weft::User`, `Envelope` | `core` | The claims, and what TypeScript sends about a call |
|
|
284
|
+
| `weft::Functions` | `core` | A service's functions by contract id |
|
|
285
|
+
| `weft::Error`, `Failure`, `Failed` | `core` | The default error, the trait an error type implements, and what it becomes on the wire |
|
|
286
|
+
|
|
287
|
+
## Troubleshooting
|
|
288
|
+
|
|
289
|
+
| Message | Cause and fix |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `R103: no .weft/contract.json above this crate` | The build script found no contract. Run `weft dev` or `weft check` in the app, or set `WEFT_CONTRACT` |
|
|
292
|
+
| `R103: …` with a function's name from `cargo` | The contract changed and the Rust no longer matches. Update the `impl Service` |
|
|
293
|
+
| `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 |
|
|
294
|
+
| `Refused`: the signature doesn't match | `WEFT_RUST_SECRET` differs between the two sides |
|
|
295
|
+
| `Refused`: signed too long ago | A clock is more than 60 seconds off |
|
|
296
|
+
| `Drift` | The sidecar and TypeScript were built from different contracts. Rebuild the sidecar |
|
|
297
|
+
|
|
298
|
+
The full list of Rust codes, R101 to R109, is in the [Rust reference](../../packages/cli/docs/rust.md).
|
|
299
|
+
|
|
300
|
+
## Build and test
|
|
301
|
+
|
|
302
|
+
The toolchain is pinned in `rust-toolchain.toml` to Rust `1.99.1` with `clippy` and `rustfmt`.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
cd crates/weft
|
|
306
|
+
cargo test # default features: unit tests, tests/router.rs and doc tests
|
|
307
|
+
cargo test --features build,otel # also the generator's and the otel layer's unit tests
|
|
308
|
+
cargo clippy --all-targets -- -D warnings
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The `build` and `otel` modules exist only with their features, so their unit tests run only when those are on.
|
|
312
|
+
|
|
313
|
+
`tests/router.rs` drives the router as TypeScript meets it: a signed POST and each way a call can end (a value with
|
|
314
|
+
both reports, the declared context enforced at run time, a typed error, a panic, the deadline, drift, an unknown
|
|
315
|
+
function, a bad signature, and the health check). `src/ctx.rs` holds a `compile_fail` doc test proving `SharedCtx` has
|
|
316
|
+
no `user()`. `evals/gates/phase5a-exit.mjs` and `evals/gates/phase5b-exit.mjs` build real crates against this one.
|
|
317
|
+
|
|
318
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for the repository workflow.
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//! Emit each function's types, errors, trait method and dispatch entry.
|
|
2
|
+
|
|
3
|
+
use std::fmt::Write as _;
|
|
4
|
+
|
|
5
|
+
use typify::TypeSpace;
|
|
6
|
+
|
|
7
|
+
use super::{Spec, add, alias};
|
|
8
|
+
|
|
9
|
+
pub(super) fn function(
|
|
10
|
+
s: &Spec<'_>,
|
|
11
|
+
space: &mut TypeSpace,
|
|
12
|
+
extra: &mut String,
|
|
13
|
+
body: &mut String,
|
|
14
|
+
methods: &mut String,
|
|
15
|
+
table: &mut String,
|
|
16
|
+
) -> Result<(), String> {
|
|
17
|
+
let name = &s.type_name;
|
|
18
|
+
let input = match &s.input {
|
|
19
|
+
Some(schema) => {
|
|
20
|
+
*extra += &alias(
|
|
21
|
+
&format!("{name}Input"),
|
|
22
|
+
&add(space, schema, &format!("{name}Input"))?,
|
|
23
|
+
);
|
|
24
|
+
format!(", input: {name}Input")
|
|
25
|
+
}
|
|
26
|
+
None => String::new(),
|
|
27
|
+
};
|
|
28
|
+
*extra += &alias(
|
|
29
|
+
&format!("{name}Output"),
|
|
30
|
+
&add(space, &s.output, &format!("{name}Output"))?,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let error = error(s, space, extra, body)?;
|
|
34
|
+
|
|
35
|
+
let _ = writeln!(
|
|
36
|
+
methods,
|
|
37
|
+
"/// `{id}`\nfn {method}(&self, ctx: ::weft::{ctx}{input}) -> impl ::std::future::Future<Output = ::std::result::Result<{name}Output, {error}>> + Send;",
|
|
38
|
+
id = s.id,
|
|
39
|
+
method = s.method,
|
|
40
|
+
ctx = s.ctx,
|
|
41
|
+
);
|
|
42
|
+
let (param, args) = if s.input.is_some() {
|
|
43
|
+
(format!("input: {name}Input"), "ctx, input")
|
|
44
|
+
} else {
|
|
45
|
+
("_: ()".to_string(), "ctx")
|
|
46
|
+
};
|
|
47
|
+
let _ = writeln!(
|
|
48
|
+
table,
|
|
49
|
+
".add({id:?}, {{ let s = service.clone(); move |ctx: ::weft::{ctx}, {param}| {{ let s = s.clone(); async move {{ s.{method}({args}).await }} }} }})",
|
|
50
|
+
id = s.id,
|
|
51
|
+
ctx = s.ctx,
|
|
52
|
+
method = s.method,
|
|
53
|
+
);
|
|
54
|
+
Ok(())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn error(
|
|
58
|
+
s: &Spec<'_>,
|
|
59
|
+
space: &mut TypeSpace,
|
|
60
|
+
extra: &mut String,
|
|
61
|
+
body: &mut String,
|
|
62
|
+
) -> Result<String, String> {
|
|
63
|
+
let name = &s.type_name;
|
|
64
|
+
Ok(if s.errors.is_empty() {
|
|
65
|
+
"::weft::Error".to_string()
|
|
66
|
+
} else {
|
|
67
|
+
let mut variants = String::new();
|
|
68
|
+
for (code, schema) in &s.errors {
|
|
69
|
+
let data = format!("{name}{code}");
|
|
70
|
+
*extra += &alias(&data, &add(space, schema, &data)?);
|
|
71
|
+
let _ = writeln!(variants, "{code}({data}),");
|
|
72
|
+
}
|
|
73
|
+
let _ = write!(
|
|
74
|
+
*body,
|
|
75
|
+
r#"
|
|
76
|
+
/// `{id}`'s declared errors, and `Internal` for the rest: any `std::error::Error` converts, so `?` works.
|
|
77
|
+
#[derive(Debug, ::serde::Serialize)]
|
|
78
|
+
#[serde(tag = "code", content = "data")]
|
|
79
|
+
pub enum {name}Error {{
|
|
80
|
+
{variants}
|
|
81
|
+
#[serde(skip)]
|
|
82
|
+
Internal(::std::string::String),
|
|
83
|
+
}}
|
|
84
|
+
impl<E: ::std::error::Error> ::std::convert::From<E> for {name}Error {{
|
|
85
|
+
fn from(e: E) -> Self {{ Self::Internal(e.to_string()) }}
|
|
86
|
+
}}
|
|
87
|
+
impl ::weft::Failure for {name}Error {{
|
|
88
|
+
fn failed(self) -> ::weft::Failed {{
|
|
89
|
+
match self {{
|
|
90
|
+
Self::Internal(detail) => ::weft::Failed::Internal(detail),
|
|
91
|
+
typed => match ::serde_json::to_value(typed) {{
|
|
92
|
+
Ok(v) => ::weft::Failed::Typed(v),
|
|
93
|
+
Err(e) => ::weft::Failed::Internal(e.to_string()),
|
|
94
|
+
}},
|
|
95
|
+
}}
|
|
96
|
+
}}
|
|
97
|
+
}}
|
|
98
|
+
"#,
|
|
99
|
+
id = s.id,
|
|
100
|
+
);
|
|
101
|
+
format!("{name}Error")
|
|
102
|
+
})
|
|
103
|
+
}
|