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

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,144 @@
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? }` (defaults `/_brust/action`, 262144). Present = actions on. */
47
+ export interface NapiActionsOptions {
48
+ prefix?: string
49
+ maxBodyBytes?: number
50
+ }
51
+
52
+ /**
53
+ * Global CORS (0.1.x `NapiCorsOptions`, brust/src/lib.rs:79-98); napi camelCases the keys:
54
+ * `origins, methods, headers, exposeHeaders, credentials, maxAgeSeconds`. Validated by `start` (`cors: …`).
55
+ */
56
+ export interface NapiCorsOptions {
57
+ origins: Array<string>
58
+ methods?: Array<string>
59
+ headers?: Array<string>
60
+ exposeHeaders?: Array<string>
61
+ credentials?: boolean
62
+ maxAgeSeconds?: number
63
+ }
64
+
65
+ export interface NapiDiagnostic {
66
+ class: string
67
+ rule: string
68
+ message: string
69
+ line: number
70
+ col: number
71
+ remediation: string
72
+ }
73
+
74
+ export interface NapiInvalidateArgs {
75
+ key?: string
76
+ tags?: Array<string>
77
+ path?: string
78
+ method?: string
79
+ }
80
+
81
+ export interface NapiInvalidateResult {
82
+ l1Removed: number
83
+ jobRemoved: number
84
+ }
85
+
86
+ /**
87
+ * M3-P P6: plan the calling worker's in-flight loader call for `slot` from the `len` bytes it
88
+ * just wrote there. Returns the misses' `JobsRequest` JSON, `""` when every job hit, or `"!"`
89
+ * (declined: answer with the loader response already in the slot). Sync, on the worker's JS
90
+ * thread; never throws (a napi Error return is unreliable under Bun).
91
+ */
92
+ export declare function planJobs(slot: number, len: number): string
93
+
94
+ /**
95
+ * Register the calling worker with the current server. `buf` is a view of
96
+ * the worker's SharedArrayBuffer, which must outlive the process's use of
97
+ * this worker (`brust start` allocates it on the main thread and keeps it for
98
+ * the process lifetime: `dispatch.rs` BufPtr); it is split into
99
+ * `slots` disjoint sub-regions of `floor(buf.byteLength / slots)` bytes.
100
+ * `f(kind, requestJson, slot)` writes the response JSON at offset
101
+ * `slot * sub` and resolves with its byte length (> 0, ≤ sub). Returns the
102
+ * worker id.
103
+ */
104
+ export declare function registerWorker(buf: Uint8Array, slots: number, f: (arg0: string, arg1: string, arg2: number) => Promise<number>): number
105
+
106
+ export interface StartOptions {
107
+ host: string
108
+ port: number
109
+ distDir: string
110
+ /** Workers to wait for before accepting; `0` = serve at once. */
111
+ workers: number
112
+ claimTimeoutMs?: number
113
+ /**
114
+ * A claimed worker call that has not settled after this long answers
115
+ * 504 (the slot stays claimed until JS settles). Absent = 30 000.
116
+ */
117
+ callTimeoutMs?: number
118
+ /**
119
+ * tokio threads of the HTTP runtime (every page renders there); absent =
120
+ * one per core (`Tuning::default`).
121
+ */
122
+ workerThreads?: number
123
+ l1Capacity?: number
124
+ jobCacheCapacity?: number
125
+ /** `X-Powered-By` value; absent = header not stamped. */
126
+ generator?: string
127
+ /** Global CORS policy; absent = CORS off. */
128
+ cors?: NapiCorsOptions
129
+ /** The actions subtree (M3-F); absent = no actions (byte-identical server). */
130
+ actions?: NapiActionsOptions
131
+ }
132
+
133
+ /**
134
+ * Boot the server (manifest, routes, templates, bind). Throws on any boot
135
+ * error (`manifest: …`, `routes: …`, `render: …`, `cors: …`, `actions: …`,
136
+ * `bind failed on …`).
137
+ */
138
+ export declare function startServer(opts: StartOptions): void
139
+
140
+ /**
141
+ * Resolves once `workers` workers registered with the current server;
142
+ * rejects after `timeout_ms` (the caller decides the exit policy).
143
+ */
144
+ export declare function untilReady(timeoutMs: number): Promise<void>