@brust/core 0.0.0-stage → 2.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,92 @@
1
- # Temporary Holding Version
1
+ # @brust/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Rust serves the pages; N Bun workers answer only `loader` and `jobs` calls. One napi addon
4
+ (`crates/brust-napi`, loader generated into `native/` by `napi build`) binds `brust-server` and
5
+ `brust-compiler`. Workspace-private (0.0.0) until m2e publishes it; peers `react`/`react-dom` 19.
6
+
7
+ ## Routes (`routes.tsx`)
8
+
9
+ ```tsx
10
+ import { defineRoutes, notFound } from '@brust/core/routes'
11
+ export const routes = defineRoutes([
12
+ { Component: AppLayout, children: [ // layout renders <Outlet />
13
+ { path: '/', Component: HomePage }, // static: never calls Bun
14
+ { path: '/items/{id}', Component: ItemPage,
15
+ loader: async ({ params }) => (params.id === 'x' ? notFound({}) : { item: await load(params.id) }),
16
+ cache: { ttl_seconds: 60, tags: ['items'] } }, // a HIT never calls Bun
17
+ ] },
18
+ ])
19
+ ```
20
+
21
+ Route fields are exactly `path`, `Component`, `loader`, `cache`, `children`; anything else fails the
22
+ build with `<field> is not supported in M2 (M3)`. Loaders run root → leaf, results merge flat
23
+ (later keys win); return `notFound(data)` / `redirect(location, status)` or throw
24
+ `httpError(status, body)` for a verdict. `cache(Comp, opts)` and `cache.invalidate(…)` ship too.
25
+
26
+ ## `brust build [entry=routes.tsx] [--out-dir dist]`
27
+
28
+ Compiles every route component through lowering, then writes (always from scratch):
29
+
30
+ ```
31
+ dist/manifest.json the only build → server contract (spec S6)
32
+ dist/jinja/<id>.jinja templates dist/jobs.js + jobs/*.server.ts worker jobs
33
+ dist/client/*-<hex>.js runtime, native chunks, react-<id> island chunks (served immutable)
34
+ dist/public/ copied from ./public dist/index.js `bun dist/index.js` = start
35
+ ```
36
+
37
+ Every route component's `children[]` also gets one static entry per native descendant (at any depth) that has a client chunk, so its `<script>` is linked once per page.
38
+
39
+ A diagnostic (`nested-instance`, `outlet-in-react`, `outlet-outside-layout`, unsupported route
40
+ field, unresolvable Component) prints `error <rule> <message>` and exits 1. The route tree is
41
+ validated whether or not the entry used `defineRoutes` (`route-config`, `duplicate-route` for two
42
+ patterns the router cannot hold together, e.g. `/a` and `/a/`).
43
+
44
+ Server-only code never reaches a browser bundle (`error server-only-in-client <importer> imports
45
+ <spec>`, at any import depth, react islands included): `node:*` / `bun:*` / bare Node builtins
46
+ (unless an npm package of that name is installed), `@brust/core/server|native`, any
47
+ `*.server.*` file, and the paths/prefixes listed in `brust.toml`:
48
+
49
+ ```toml
50
+ [build]
51
+ server_only = ["lib/server", "@acme/db"] # import prefixes, or paths relative to the app root
52
+ ```
53
+
54
+ Loaders and precompute jobs may use all of it (they run in Bun). In the browser `@brust/core`
55
+ resolves to a side-effect-free entry (`cache`, route helpers, verdicts; `cache.invalidate` throws).
56
+
57
+ The out dir is rebuilt in a sibling temp dir and swapped in only on success (a failed build leaves
58
+ the previous one untouched); an out dir that is the filesystem root, the home dir, the app root or
59
+ an ancestor of it, or that holds the entry / a route Component / `public/`, is refused
60
+ (`out-dir-unsafe`). Relative imports of
61
+ generated files resolve against the component's source dir, bare imports against the app root, so
62
+ `--out-dir` may point anywhere; at run time `dist/` must still resolve `react` and `@brust/core`
63
+ (keep it inside the app, or next to a `node_modules` that has them).
64
+
65
+ ## `brust start [--port N] [--workers N] [--dist-dir dist] [--entry routes.tsx]`
66
+
67
+ Prints `[brust] listening on <addr>` then `[brust] ready (N workers)`. `--port 0` picks a free port.
68
+ SIGINT/SIGTERM drains gracefully (`BRUST_DRAIN_TIMEOUT_MS`); a second signal exits at once.
69
+ Config precedence: env (`BRUST_ADDR`, `BRUST_PORT`, `BRUST_WORKERS`, `BRUST_RENDER_SLOTS`,
70
+ `BRUST_DRAIN_TIMEOUT_MS`, `BRUST_BOOT_TIMEOUT_MS`, `BRUST_CALL_TIMEOUT_MS`, `BRUST_DIST_DIR`, `BRUST_APP_ENTRY`) > flags
71
+ (`--port`, `--workers`, `--dist-dir`, `--entry`) > `brust.toml` (`[server] address/port`,
72
+ `[workers] count`) > defaults (`localhost:1337`, one worker per CPU, `min(cores, 16)` slots per worker (each slot is a 256 KiB shared response buffer, so 10 slots = 2.5 MiB per worker), 10 s drain, 30 s
73
+ worker boot timeout, 30 s call timeout).
74
+ A loader or job call that has not settled within `BRUST_CALL_TIMEOUT_MS` answers **504**; its worker
75
+ slot stays claimed until the call settles (the late result is discarded), so a stuck call costs one
76
+ slot, never a frozen worker. Waiting for a free slot is the separate 503 "all workers busy".
77
+ `GET /_brust/cache/stats` reports L1/job cache counters plus `loader_calls` / `job_calls` /
78
+ `timed_out_calls`.
79
+
80
+ ## Worker wire (contract 7, `crates/brust-server/src/protocol.rs`)
81
+
82
+ Requests arrive as inline JSON; responses go into the worker's SharedArrayBuffer slot (never
83
+ past `floor(len / slots)` bytes: an oversize response becomes `{"error":…}`). A handler never rejects.
84
+
85
+ `loader` `{routeId, params, path, req}` → `{ok:true, data}` | `{verdict, …}` | `{error}`;
86
+ `jobs` `{jobs:[{id, componentId, kind, inputs, target?, row?}]}` → `{results:[{id, value}|{id, error}]}`
87
+ (an `ssr` job renders `jobs[target]` with the manifest job's `literals` merged over `inputs`).
88
+
89
+ **M2 limits:** the route fields above only; per-row child instances one level deep (a nested
90
+ instance is a build error); a react child's props must be props paths or literals; no worker respawn.
91
+ **Gates:** `bun run build:debug && bun run typecheck && bun test` (CI runs each server-starting
92
+ file alone; e2e: `bun test --timeout 120000 test/e2e.test.ts`).
package/bin/brust ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import { main } from '../src/cli.ts'
3
+
4
+ await main(process.argv.slice(2))
@@ -0,0 +1,162 @@
1
+ /* auto-generated by NAPI-RS */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Which binding artifact the generated loader actually loaded: `'native'` for
6
+ * a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
7
+ * flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
8
+ * can point the loader at a WASI artifact this package does not build itself.
9
+ */
10
+ export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
11
+
12
+ /**
13
+ * Graceful drain of the current server: stop accepting, wait for in-flight
14
+ * connections up to `timeout_ms`.
15
+ */
16
+ export declare function beginDrain(timeoutMs: number): Promise<void>
17
+
18
+ /** `cache.invalidate({ key?, tags?, path?, method? })` (spec §5). */
19
+ export declare function cacheInvalidate(args: NapiInvalidateArgs): NapiInvalidateResult
20
+
21
+ /** `/_brust/cache/stats` JSON (snake_case keys, as the server serialises it). */
22
+ export declare function cacheStats(): string
23
+
24
+ export interface CompiledComponent {
25
+ id: string
26
+ source: string
27
+ /** `ComponentIR` as JSON (snake_case keys, as the compiler serialises it). */
28
+ ir: string
29
+ jinja: string
30
+ serverTs?: string
31
+ clientJs?: string
32
+ diagnostics: Array<NapiDiagnostic>
33
+ }
34
+
35
+ export interface CompiledTree {
36
+ components: Array<CompiledComponent>
37
+ error?: NapiDiagnostic
38
+ }
39
+
40
+ /** Compiles `path` (relative to `root`) and every child it reaches through lowering. */
41
+ export declare function compileTree(path: string, root: string, runtimeImport: string, serverOnly: Array<string>): CompiledTree
42
+
43
+ /** The bound address as `host:port` (port 0 resolves here). */
44
+ export declare function localAddr(): string
45
+
46
+ /** M3-F A3 consumer `{ prefix?, maxBodyBytes?, maxResponseBytes? }` (defaults `/_brust/action`, 1 MiB, 8 MiB). Present = actions on. */
47
+ export interface NapiActionsOptions {
48
+ prefix?: string
49
+ maxBodyBytes?: number
50
+ /** F82: cap on an action response body (default 8388608). */
51
+ maxResponseBytes?: number
52
+ /** `"reject"` (default) or `"allow"` (F87): cross-site mutating action requests. */
53
+ crossSite?: string
54
+ }
55
+
56
+ /**
57
+ * Global CORS (0.1.x `NapiCorsOptions`, brust/src/lib.rs:79-98); napi camelCases the keys:
58
+ * `origins, methods, headers, exposeHeaders, credentials, maxAgeSeconds`. Validated by `start` (`cors: …`).
59
+ */
60
+ export interface NapiCorsOptions {
61
+ origins: Array<string>
62
+ methods?: Array<string>
63
+ headers?: Array<string>
64
+ exposeHeaders?: Array<string>
65
+ credentials?: boolean
66
+ maxAgeSeconds?: number
67
+ }
68
+
69
+ export interface NapiDiagnostic {
70
+ class: string
71
+ rule: string
72
+ message: string
73
+ line: number
74
+ col: number
75
+ remediation: string
76
+ }
77
+
78
+ export interface NapiInvalidateArgs {
79
+ key?: string
80
+ tags?: Array<string>
81
+ path?: string
82
+ method?: string
83
+ }
84
+
85
+ export interface NapiInvalidateResult {
86
+ l1Removed: number
87
+ jobRemoved: number
88
+ }
89
+
90
+ /**
91
+ * M3-P P6: plan the calling worker's in-flight loader call for `slot` from the `len` bytes it
92
+ * just wrote there. Returns the misses' `JobsRequest` JSON, `""` when every job hit, or `"!"`
93
+ * (declined: answer with the loader response already in the slot). Sync, on the worker's JS
94
+ * thread; never throws (a napi Error return is unreliable under Bun).
95
+ */
96
+ export declare function planJobs(slot: number, len: number): string
97
+
98
+ /**
99
+ * F82: hand Rust the response body of the calling worker's in-flight `fetch` call for `slot`
100
+ * (too large for the SAB slot). `false` = declined (over `max_response_bytes`, not in a `fetch`
101
+ * call, already put, nothing bound). Sync, on the worker's JS thread; never throws.
102
+ */
103
+ export declare function putFetchReply(slot: number, body: Uint8Array): boolean
104
+
105
+ /**
106
+ * Register the calling worker with the current server. `buf` is a view of
107
+ * the worker's SharedArrayBuffer, which must outlive the process's use of
108
+ * this worker (`brust start` allocates it on the main thread and keeps it for
109
+ * the process lifetime: `dispatch.rs` BufPtr); it is split into
110
+ * `slots` disjoint sub-regions of `floor(buf.byteLength / slots)` bytes.
111
+ * `f(kind, requestJson, slot)` writes the response JSON at offset
112
+ * `slot * sub` and resolves with its byte length (> 0, ≤ sub). Returns the
113
+ * worker id.
114
+ */
115
+ export declare function registerWorker(buf: Uint8Array, slots: number, f: (arg0: string, arg1: string, arg2: number) => Promise<number>): number
116
+
117
+ export interface StartOptions {
118
+ host: string
119
+ port: number
120
+ distDir: string
121
+ /** Workers to wait for before accepting; `0` = serve at once. */
122
+ workers: number
123
+ claimTimeoutMs?: number
124
+ /**
125
+ * A claimed worker call that has not settled after this long answers
126
+ * 504 (the slot stays claimed until JS settles). Absent = 30 000.
127
+ */
128
+ callTimeoutMs?: number
129
+ /**
130
+ * tokio threads of the HTTP runtime (every page renders there); absent =
131
+ * one per core (`Tuning::default`).
132
+ */
133
+ workerThreads?: number
134
+ l1Capacity?: number
135
+ jobCacheCapacity?: number
136
+ /** `X-Powered-By` value; absent = header not stamped. */
137
+ generator?: string
138
+ /** Global CORS policy; absent = CORS off. */
139
+ cors?: NapiCorsOptions
140
+ /** The actions subtree (M3-F); absent = no actions (byte-identical server). */
141
+ actions?: NapiActionsOptions
142
+ }
143
+
144
+ /**
145
+ * Boot the server (manifest, routes, templates, bind). Throws on any boot
146
+ * error (`manifest: …`, `routes: …`, `render: …`, `cors: …`, `actions: …`,
147
+ * `bind failed on …`).
148
+ */
149
+ export declare function startServer(opts: StartOptions): void
150
+
151
+ /**
152
+ * F82: the heap request body of the calling worker's in-flight `fetch` call for `slot`, once.
153
+ * `None` when there is none, nothing is bound, or the binding is ambiguous. Sync, on the
154
+ * worker's JS thread; never throws.
155
+ */
156
+ export declare function takeFetchBody(slot: number): Uint8Array | null
157
+
158
+ /**
159
+ * Resolves once `workers` workers registered with the current server;
160
+ * rejects after `timeout_ms` (the caller decides the exit policy).
161
+ */
162
+ export declare function untilReady(timeoutMs: number): Promise<void>