@weft-js/start 0.0.0-stage → 0.1.0

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 (75) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +41 -2
  3. package/package.json +107 -4
  4. package/src/app.mjs +82 -0
  5. package/src/auth/better-auth.ts +58 -0
  6. package/src/auth/drizzle.ts +17 -0
  7. package/src/auth.ts +47 -0
  8. package/src/boot.ts +44 -0
  9. package/src/builtin.tsx +19 -0
  10. package/src/bus/postgres.ts +91 -0
  11. package/src/bus.ts +48 -0
  12. package/src/capture.d.mts +1 -0
  13. package/src/capture.mjs +15 -0
  14. package/src/changes-sql.d.mts +14 -0
  15. package/src/changes-sql.mjs +99 -0
  16. package/src/changes.ts +253 -0
  17. package/src/channel-fake.ts +55 -0
  18. package/src/channel.ts +616 -0
  19. package/src/client.tsx +20 -0
  20. package/src/contract.mjs +308 -0
  21. package/src/data.ts +929 -0
  22. package/src/db/bun-sqlite.d.ts +10 -0
  23. package/src/db/changes-mysql.ts +86 -0
  24. package/src/db/changes-postgres.ts +159 -0
  25. package/src/db/changes-sqlite.ts +116 -0
  26. package/src/db/core.ts +63 -0
  27. package/src/db/index.ts +27 -0
  28. package/src/db/mysql.ts +67 -0
  29. package/src/db/postgres.ts +65 -0
  30. package/src/db/sqlite.ts +139 -0
  31. package/src/derive.d.mts +16 -0
  32. package/src/derive.mjs +140 -0
  33. package/src/dev-runtime.mjs +42 -0
  34. package/src/exec.ts +519 -0
  35. package/src/extension.ts +38 -0
  36. package/src/first-paint.ts +57 -0
  37. package/src/form.tsx +134 -0
  38. package/src/gate.css +6 -0
  39. package/src/hub.ts +822 -0
  40. package/src/index.ts +106 -0
  41. package/src/inline.mjs +2 -0
  42. package/src/inspect.mjs +280 -0
  43. package/src/link.ts +456 -0
  44. package/src/list-drizzle.ts +50 -0
  45. package/src/list.ts +224 -0
  46. package/src/listen.ts +28 -0
  47. package/src/live-fake.ts +42 -0
  48. package/src/live-target.mjs +144 -0
  49. package/src/live.ts +499 -0
  50. package/src/media.ts +43 -0
  51. package/src/migrations.mjs +19 -0
  52. package/src/mux-session.ts +6 -0
  53. package/src/mux.ts +135 -0
  54. package/src/og/DefaultCard.tsx +60 -0
  55. package/src/og/fonts.mjs +64 -0
  56. package/src/og/images.mjs +54 -0
  57. package/src/og/index.mjs +146 -0
  58. package/src/og/native.mjs +29 -0
  59. package/src/range.mjs +19 -0
  60. package/src/router.ts +149 -0
  61. package/src/routes.mjs +142 -0
  62. package/src/run.tsx +198 -0
  63. package/src/rust.ts +333 -0
  64. package/src/server.tsx +1014 -0
  65. package/src/shape.d.mts +3 -0
  66. package/src/shape.mjs +73 -0
  67. package/src/share-server.ts +208 -0
  68. package/src/share.ts +503 -0
  69. package/src/sheets.mjs +75 -0
  70. package/src/sidecar.mjs +503 -0
  71. package/src/socket.ts +389 -0
  72. package/src/stream.ts +13 -0
  73. package/src/verify.ts +251 -0
  74. package/src/view.tsx +47 -0
  75. package/src/vite.mjs +899 -0
package/src/rust.ts ADDED
@@ -0,0 +1,333 @@
1
+ // `remote()` and `native()` (PLAN §4.3, research 36 and 37): a server function whose body is Rust, in a sidecar over
2
+ // `fetch`, or in this process. One crossing per function. TS keeps the public edge: the input is validated before the
3
+ // call and the output after it, the user's claims go along (never cookies; signed for a sidecar), and the caches stay here.
4
+ //
5
+ // // src/server/rust.ts: the only place Rust targets are named
6
+ // export const search = remote('rust://search')
7
+ // export const media = native('media') // an addon, or its .wasm where an addon can't load
8
+ //
9
+ // // src/server/docs.ts: swap `run` for `impl`, and nothing on the client changes
10
+ // export const searchDocs = query({ input: Search, output: z.array(Hit), tags: ['docs'], impl: search })
11
+ //
12
+ // A sidecar's address and key are the environment's: `weft dev` and `weft start` set `WEFT_RUST_<NAME>_URL` and
13
+ // `WEFT_RUST_SECRET` for the sidecars they supervise, and a sidecar deployed on its own needs both set by hand.
14
+ // Nothing here imports a Node module, so none of it reaches a bundle that doesn't call it.
15
+
16
+ import { raise, StartError, type Context, type Fn } from './data'
17
+
18
+ /** Whoever `ctx.user` is in this app, once it declares `Context`. */
19
+ type User = Context extends { user: infer U } ? U | null : unknown
20
+
21
+ /** What a sidecar may know of the user. Sent signed; a sidecar never sees a cookie or a session token. */
22
+ export interface Claims { id: string; roles?: string[]; tenant?: string | null; [key: string]: unknown }
23
+
24
+ /** `remote()`: the address, the deadline, and how a user becomes claims. */
25
+ export interface RemoteOptions {
26
+ /** The address, for a sidecar that isn't supervised here. Otherwise `WEFT_RUST_<NAME>_URL`. */
27
+ url?: string
28
+ /** Milliseconds a call may take, sent as its deadline. 10 s by default. */
29
+ timeout?: number
30
+ /** What a sidecar gets of `ctx.user`. By default `id`, `roles` (or `role`) and `tenant`. */
31
+ claims?: (user: User) => Claims | null
32
+ /** `false` trusts a sidecar's output. It's the only check that a sidecar still matches the contract: keep it on. */
33
+ validate?: boolean
34
+ }
35
+
36
+ /** One function call on its way to a sidecar. */
37
+ export interface RemoteCall { fn: Fn; input: unknown; ctx: Context; request: Request }
38
+ /** What a sidecar answered: the value, and the tags it says to refresh. */
39
+ export interface RemoteResult { value: unknown; tags: string[]; touched: string[] }
40
+
41
+ /** What `remote()` returns: pass it as a function's `impl`. */
42
+ export interface Remote {
43
+ readonly kind: 'remote'
44
+ readonly target: string
45
+ readonly service: string
46
+ call(c: RemoteCall): Promise<RemoteResult>
47
+ }
48
+
49
+ /** `native()`: the deadline, and how a user becomes claims. */
50
+ export interface NativeOptions {
51
+ /** Milliseconds TS waits. A call can't be stopped: `ctx.cancelled()` tells the Rust. 10 s by default. */
52
+ timeout?: number
53
+ /** What a function gets of `ctx.user`, as for `remote()`. */
54
+ claims?: (user: User) => Claims | null
55
+ /** `false` trusts the output. Keep it on: it's a check on every call, and small next to the work. */
56
+ validate?: boolean
57
+ }
58
+
59
+ /** What `native()` returns: pass it as a function's `impl`. */
60
+ export interface Native {
61
+ readonly kind: 'native'
62
+ readonly target: string
63
+ readonly service: string
64
+ /** What's serving, once a call has loaded it: `addon` or `wasm`. For a log line or a test. */
65
+ engine(): Promise<'addon' | 'wasm'>
66
+ call(c: RemoteCall): Promise<RemoteResult>
67
+ }
68
+
69
+ const TARGET = /^rust:\/\/([a-z][a-z0-9_-]*)$/
70
+ const NAME = /^[a-z][a-z0-9_-]*$/
71
+ const DEFAULT_TIMEOUT = 10_000
72
+ const encoder = new TextEncoder()
73
+
74
+ // An app's tsc may have no Node types, so what's read of `process` is named here.
75
+ const nodeHost = globalThis as unknown as { process?: { env: Record<string, string | undefined>; platform: string; arch: string; getBuiltinModule?(name: string): unknown; dlopen(mod: object, file: string): void } }
76
+ const env = (name: string): string | undefined => nodeHost.process?.env?.[name] || undefined
77
+ /** `search-index` → `WEFT_RUST_SEARCH_INDEX_URL`. */
78
+ export const urlVariable = (service: string) => `WEFT_RUST_${service.toUpperCase().replaceAll('-', '_')}_URL`
79
+
80
+ const hex = (b: ArrayBuffer) => Array.from(new Uint8Array(b), x => x.toString(16).padStart(2, '0')).join('')
81
+ const base64url = (s: string) => btoa(String.fromCharCode(...encoder.encode(s))).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')
82
+ const randomHex = (bytes: number) => Array.from(crypto.getRandomValues(new Uint8Array(bytes)), x => x.toString(16).padStart(2, '0')).join('')
83
+
84
+ const keys = new Map<string, Promise<CryptoKey>>()
85
+ const keyOf = (secret: string) => {
86
+ let k = keys.get(secret)
87
+ if (!k) keys.set(secret, (k = crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])))
88
+ return k
89
+ }
90
+
91
+ /** `weft-sig`: the crate's `verify` checks the same bytes. It covers the function and the body, not just the claims. */
92
+ export const sign = async (secret: string, id: string, ctx: string, body: string) =>
93
+ hex(await crypto.subtle.sign('HMAC', await keyOf(secret), encoder.encode(`weft1\n${id}\n${ctx}\n${body}`)))
94
+
95
+ const defaultClaims = (user: User): Claims | null => {
96
+ const u = user as { id: unknown; roles?: string[]; role?: string; tenant?: string | null } | null
97
+ return u && { id: String(u.id), roles: u.roles ?? (u.role ? [u.role] : []), ...(u.tenant != null && { tenant: u.tenant }) }
98
+ }
99
+
100
+ const TRACEPARENT = /^00-(?!0{32})[0-9a-f]{32}-(?!0{16})[0-9a-f]{16}-[0-9a-f]{2}$/
101
+
102
+ /** The caller's trace with a new span, or a new trace. */
103
+ function traceparent(request: Request): string {
104
+ const inbound = request.headers.get('traceparent')
105
+ const trace = inbound && TRACEPARENT.test(inbound) ? inbound.split('-')[1] : randomHex(16)
106
+ return `00-${trace}-${randomHex(8)}-01`
107
+ }
108
+
109
+ /** The value as a sidecar reads it: what the schema parsed, encoded back to the wire shape (codecs, defaults filled). */
110
+ function wire(fn: Fn, input: unknown): unknown {
111
+ const schema = fn.def.input as { encodeAsync?: (value: unknown) => Promise<unknown> } | undefined
112
+ if (typeof schema?.encodeAsync === 'function') return schema.encodeAsync(input)
113
+ return input
114
+ }
115
+
116
+ async function checkedOutput(fn: Fn, value: unknown, validate: boolean): Promise<unknown> {
117
+ const schema = fn.def.output
118
+ if (!schema || !validate) return value
119
+ const r = await schema['~standard'].validate(value)
120
+ if (!r.issues) return r.value
121
+ const issues = r.issues.map(i => `${i.path?.map(p => (typeof p === 'object' ? p.key : p)).join('.') ?? ''}: ${i.message}`).join('; ')
122
+ throw new StartError('R103', `\`${fn.$id}\`'s sidecar returned a value its \`output\` schema rejects: ${issues}`, 'the sidecar was built from another contract, or sets a field the schema refuses: rebuild it, or fix the schema', { status: 502 })
123
+ }
124
+
125
+ /** A declared error: its data checked like any output, with the status a client can act on. */
126
+ async function declared(fn: Fn, error: { code: string; data?: unknown }): Promise<never> {
127
+ const schema = fn.def.errors?.[error.code]
128
+ if (!schema) throw new StartError('R103', `\`${fn.$id}\`'s sidecar answered with the undeclared error \`${error.code}\``, 'declare it in `errors`, or rebuild the sidecar from the current contract', { status: 502 })
129
+ const r = await schema['~standard'].validate(error.data ?? null)
130
+ if (r.issues) throw new StartError('R103', `\`${fn.$id}\`'s sidecar sent \`${error.code}\` with data its schema rejects`, 'rebuild the sidecar from the current contract', { status: 502 })
131
+ raise(error.code, r.value)
132
+ }
133
+
134
+ /** A shared value goes by no one's claims: the crate's `SharedCtx` has no user to read, and neither has the wire. */
135
+ function callContext(fn: Fn, ctx: Context, request: Request, timeout: number, claims: (user: User) => Claims | null) {
136
+ const shared = fn.def.cache && fn.def.cache.scope !== 'user'
137
+ return { user: shared ? null : claims((ctx as { user?: User }).user ?? null) ?? null, requestId: randomHex(8), deadlineMs: timeout, traceparent: traceparent(request) }
138
+ }
139
+
140
+ /** What a reply comes to: the same for a sidecar's body and an addon's string. */
141
+ async function settled(fn: Fn, reply: Reply | undefined, target: string, timeout: number, validate: boolean): Promise<RemoteResult> {
142
+ if (reply?.ok === true) return { value: await checkedOutput(fn, reply.value, validate), tags: reply.tags ?? [], touched: reply.touched ?? [] }
143
+ const error = reply?.error
144
+ if (error?.code === 'Refused') throw new StartError('R106', `\`${target}\` refused \`${fn.$id}\`'s signature`, '`WEFT_RUST_SECRET` must be the same on both sides, and the clocks within a minute', { status: 503 })
145
+ if (error?.code === 'Deadline') throw new StartError('R107', `\`${fn.$id}\` ran past its ${timeout} ms`, 'raise `timeout`, or make the Rust answer sooner', { status: 504 })
146
+ if (error?.code === 'Drift') throw new StartError('R103', `\`${fn.$id}\` doesn't match \`${target}\`: ${error.message}`, 'rebuild it from the current contract', { status: 502 })
147
+ if (error?.code && error.code !== 'Internal') await declared(fn, { code: error.code, data: error.data })
148
+ // The detail stays in a log, under this id: a sidecar's own, or this server's for what ran in this process.
149
+ if (error?.detail) console.error(`[weft] \`${fn.$id}\` failed (request ${error.requestId}): ${error.detail}`)
150
+ throw new StartError('R108', `\`${fn.$id}\` failed in \`${target}\` (request ${error?.requestId ?? 'unknown'})`, 'find the request id in the server\'s or the sidecar\'s log', { status: 500 })
151
+ }
152
+
153
+ /**
154
+ * A Rust sidecar implementing the functions that point at it, as `impl: search`. `run` and `impl` are one or the other:
155
+ * the declaration, the client and the caches are the same for both.
156
+ */
157
+ export function remote(target: string, options: RemoteOptions = {}): Remote {
158
+ const service = TARGET.exec(target)?.[1]
159
+ if (!service) throw new StartError('R106', `\`remote('${target}')\` isn't a sidecar target`, "`remote('rust://search')`, with the address in `WEFT_RUST_SEARCH_URL` or the `url` option")
160
+ const { timeout = DEFAULT_TIMEOUT, claims = defaultClaims, validate = true } = options
161
+ return {
162
+ kind: 'remote', target, service,
163
+ async call({ fn, input, ctx, request }) {
164
+ const url = options.url ?? env(urlVariable(service))
165
+ if (!url) throw new StartError('R106', `\`${fn.$id}\` is implemented by \`${target}\`, and this server has no address for it`, `set \`${urlVariable(service)}\` (\`weft dev\` and \`weft start\` do it for a sidecar they supervise), or pass \`url\` to \`remote()\``, { status: 503 })
166
+ const secret = env('WEFT_RUST_SECRET')
167
+ if (!secret) throw new StartError('R106', `\`${fn.$id}\` can't sign its call: \`WEFT_RUST_SECRET\` isn't set`, 'set the same value on the server and the sidecar', { status: 503 })
168
+
169
+ const envelope = base64url(JSON.stringify({ ...callContext(fn, ctx, request, timeout, claims), iat: Date.now() }))
170
+ const body = JSON.stringify({ input: (await wire(fn, input)) ?? null })
171
+
172
+ let res: Response
173
+ try {
174
+ res = await fetch(`${url.replace(/\/$/, '')}/_weft/fn/${encodeURIComponent(fn.$id)}`, {
175
+ method: 'POST',
176
+ headers: { 'content-type': 'application/json', 'weft-ctx': envelope, 'weft-sig': await sign(secret, fn.$id, envelope, body) },
177
+ body,
178
+ signal: AbortSignal.any([AbortSignal.timeout(timeout), request.signal]),
179
+ })
180
+ } catch (e) {
181
+ if ((e as Error | undefined)?.name === 'TimeoutError') throw new StartError('R107', `\`${fn.$id}\` took longer than its ${timeout} ms`, 'raise `timeout` on `remote()`, or make the sidecar answer sooner', { status: 504 })
182
+ throw new StartError('R107', `\`${fn.$id}\`: \`${target}\` didn't answer`, 'check that the sidecar is running. Nothing else on this server is affected', { status: 503 })
183
+ }
184
+ return settled(fn, await res.json().catch(() => undefined), target, timeout, validate)
185
+ },
186
+ }
187
+ }
188
+
189
+ // ---- native ---------------------------------------------------------------------------------------------------------
190
+
191
+ /** The exports a `.wasm` built by the crate has. */
192
+ interface Wasm { memory: WebAssembly.Memory; weft_init: () => void; weft_free: (ptr: number, len: number) => void; weft_alloc: (len: number) => number; weft_contract: () => bigint; weft_call: (i: number, il: number, c: number, cl: number, b: number, bl: number) => bigint }
193
+ interface Reply { ok?: boolean; value?: unknown; tags?: string[]; touched?: string[]; error?: { code?: string; message?: string; detail?: string; requestId?: string; data?: unknown } }
194
+ interface Engine { kind: 'addon' | 'wasm'; call(id: string, ctx: string, body: string): Promise<string> | string }
195
+ interface Entry { kind?: string; hash: string; addon?: string; wasm?: string; failure?: string }
196
+
197
+ interface ReadFile { (file: string, encoding: 'utf8'): string; (file: string): Uint8Array<ArrayBuffer> }
198
+ interface Builtins { 'node:fs': { existsSync(file: string): boolean; readFileSync: ReadFile }; 'node:path': { dirname(path: string): string; join(...parts: string[]): string }; 'node:url': { fileURLToPath(url: string): string } }
199
+ const builtin = <K extends keyof Builtins>(name: K) => nodeHost.process?.getBuiltinModule?.(name) as Builtins[K] | undefined
200
+ const platform = () => { const p = nodeHost.process; return `${p?.platform}-${p?.arch}` }
201
+
202
+ /** `.output/server/rust/` from wherever the bundle put this code. `WEFT_NATIVE_DIR` is `weft dev`'s, and `weft build`'s. */
203
+ function nativeDir(): string | undefined {
204
+ const fs = builtin('node:fs'), path = builtin('node:path')!, url = builtin('node:url')!
205
+ if (!fs) return undefined
206
+ const set = env('WEFT_NATIVE_DIR')
207
+ if (set) return set
208
+ let dir = path.dirname(url.fileURLToPath(import.meta.url))
209
+ for (let i = 0; i < 6; i++, dir = path.dirname(dir)) if (fs.existsSync(path.join(dir, 'rust', 'manifest.json'))) return path.join(dir, 'rust')
210
+ return undefined
211
+ }
212
+
213
+ function addonEngine(file: string, hash: string, target: string): Engine {
214
+ const mod = { exports: {} as { contract: () => string; call: Engine['call'] } }
215
+ nodeHost.process!.dlopen(mod, file)
216
+ const built = JSON.parse(mod.exports.contract())
217
+ if (built.hash !== hash) throw new StartError('R103', `\`${target}\`'s library was built from contract ${built.hash}, and this app's is ${hash}`, 'rebuild it: `weft build`', { status: 502 })
218
+ return { kind: 'addon', call: mod.exports.call }
219
+ }
220
+
221
+ /**
222
+ * The same functions as a `.wasm`, on this thread. A panic traps, which ends an instance: what it said arrives first,
223
+ * and the next call gets a new one, as a sidecar's crash gets a restart.
224
+ */
225
+ async function wasmEngine(file: string, hash: string, target: string): Promise<Engine> {
226
+ const module = await WebAssembly.compile(builtin('node:fs')!.readFileSync(file))
227
+ const decoder = new TextDecoder()
228
+ let said = ''
229
+ let x: Wasm | undefined
230
+ const text = (ptr: number, len: number) => decoder.decode(new Uint8Array(x!.memory.buffer, ptr, len))
231
+ const fresh = () => {
232
+ x = new WebAssembly.Instance(module, { weft: { now: () => performance.now(), panicked: (ptr: number, len: number) => { said = text(ptr, len) } } }).exports as unknown as Wasm
233
+ x.weft_init()
234
+ }
235
+ const taken = (packed: bigint) => {
236
+ const ptr = Number(packed >> 32n), len = Number(packed & 0xffffffffn)
237
+ const out = text(ptr, len)
238
+ x!.weft_free(ptr, len)
239
+ return out
240
+ }
241
+ const put = (s: string): [number, number] => {
242
+ const bytes = encoder.encode(s)
243
+ const ptr = x!.weft_alloc(bytes.length)
244
+ new Uint8Array(x!.memory.buffer, ptr, bytes.length).set(bytes)
245
+ return [ptr, bytes.length]
246
+ }
247
+ fresh()
248
+ const built = JSON.parse(taken(x!.weft_contract()))
249
+ if (built.hash !== hash) throw new StartError('R103', `\`${target}\`'s library was built from contract ${built.hash}, and this app's is ${hash}`, 'rebuild it: `weft build`', { status: 502 })
250
+ return {
251
+ kind: 'wasm',
252
+ call(id, ctx, body) {
253
+ if (!x) fresh()
254
+ const [i, il] = put(id), [c, cl] = put(ctx), [b, bl] = put(body)
255
+ try {
256
+ const reply = taken(x!.weft_call(i, il, c, cl, b, bl))
257
+ x!.weft_free(i, il)
258
+ x!.weft_free(c, cl)
259
+ return reply
260
+ } catch (e) {
261
+ if (!(e instanceof WebAssembly.RuntimeError)) throw e
262
+ x = undefined
263
+ return JSON.stringify({ ok: false, error: { code: 'Internal', requestId: JSON.parse(ctx).requestId, detail: said || e.message } })
264
+ }
265
+ },
266
+ }
267
+ }
268
+
269
+ /**
270
+ * A Rust library implementing the functions that point at it, in this process, as `impl: media`. An addon runs each call
271
+ * on the worker pool, so the event loop stays free; where one can't load, its `.wasm` runs them on this thread. Either
272
+ * way the declaration, the client and the caches are the same as for `run`.
273
+ */
274
+ export function native(name: string, options: NativeOptions = {}): Native {
275
+ if (!NAME.test(name)) throw new StartError('R104', `\`native('${name}')\` isn't a service name`, "`native('media')`, for the crate at `crates/media`")
276
+ const { timeout = DEFAULT_TIMEOUT, claims = defaultClaims, validate = true } = options
277
+ const target = `native('${name}')`
278
+ // Before the pool's first use: its default of 4 is a ceiling on calls at once that an app would never think to raise.
279
+ const proc = nodeHost.process
280
+ if (proc?.env && !proc.env.UV_THREADPOOL_SIZE) proc.env.UV_THREADPOOL_SIZE = String(Math.max(4, globalThis.navigator?.hardwareConcurrency ?? 4))
281
+
282
+ let loaded: Promise<Engine> | undefined, stamp: string | undefined
283
+
284
+ async function load(dir: string, entry: Entry): Promise<Engine> {
285
+ const fs = builtin('node:fs')!, path = builtin('node:path')!
286
+ const addon = entry.addon && path.join(dir, entry.addon)
287
+ if (addon && fs.existsSync(addon)) {
288
+ try {
289
+ return addonEngine(addon, entry.hash, target)
290
+ } catch (e) {
291
+ if (e instanceof StartError) throw e
292
+ console.warn(`[weft] ${target}: the addon didn't load here (${String((e as { message?: unknown }).message).split('\n')[0]})${entry.wasm ? ': using its WASM' : ''}`)
293
+ }
294
+ }
295
+ if (entry.wasm) return wasmEngine(path.join(dir, entry.wasm), entry.hash, target)
296
+ throw new StartError('R104', `\`${target}\` has no addon for ${platform()} and no WASM`, 'build on this platform, drop `wasm: false` from `rust.services`, or use `remote()`', { status: 503 })
297
+ }
298
+
299
+ /** The engine for now. Under `weft dev` a rebuild is a new file: the next call loads it, and calls on the old one finish. */
300
+ function engine(): Promise<Engine> {
301
+ const fs = builtin('node:fs')!, path = builtin('node:path')!
302
+ const dir = nativeDir()
303
+ if (!dir) throw new StartError('R104', `\`${target}\` has nothing to load here`, 'this runtime has no file system, or the build has no `rust/` folder: use `remote()`, or run `weft build`', { status: 503 })
304
+ const file = path.join(dir, 'manifest.json')
305
+ const watching = !!env('WEFT_NATIVE_DIR')
306
+ // Its text, not its mtime: two writes can land in one tick of a coarse clock.
307
+ const text: string | undefined = watching && fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : undefined
308
+ if (loaded && text === stamp) return loaded
309
+ const entry: Entry | undefined = fs.existsSync(file) ? JSON.parse(text ?? fs.readFileSync(file, 'utf8'))[name] : undefined
310
+ if (!entry) throw new StartError('R104', `\`${target}\` isn't in this build`, 'run `weft build`, or check `crates/' + name + '`', { status: 503 })
311
+ if (entry.failure) throw new StartError('R103', `\`${target}\` didn't build: ${entry.failure}`, 'fix the crate: `weft dev` retries on every edit', { status: 503 })
312
+ stamp = text
313
+ const held = loaded
314
+ const next = load(dir, entry)
315
+ // A bad build keeps what was serving, as a sidecar's does.
316
+ loaded = next.catch(e => { if (held) { loaded = held; return held } loaded = undefined; throw e })
317
+ return loaded
318
+ }
319
+
320
+ return {
321
+ kind: 'native', target: `native://${name}`, service: name,
322
+ engine: async () => (await engine()).kind,
323
+ async call({ fn, input, ctx, request }) {
324
+ const context = JSON.stringify(callContext(fn, ctx, request, timeout, claims))
325
+ const body = JSON.stringify({ input: (await wire(fn, input)) ?? null })
326
+ const running = (await engine()).call(fn.$id, context, body)
327
+ let timer: ReturnType<typeof setTimeout> | undefined
328
+ const reply = typeof running === 'string' ? running : await Promise.race([running, new Promise<string>(ok => { timer = setTimeout(ok, timeout, '{"ok":false,"error":{"code":"Deadline"}}') })])
329
+ clearTimeout(timer)
330
+ return settled(fn, JSON.parse(reply), target, timeout, validate)
331
+ },
332
+ }
333
+ }