server.ts135 lines · main
| 1 | // Runs inside the Deno isolate. Materialized by host into |
| 2 | // /tmp/briven-isolate-<id>/.briven-runtime/server.ts. Customers reach |
| 3 | // this via `import { query } from '@briven/cli/server'` resolved through |
| 4 | // the host-controlled import-map.json. |
| 5 | |
| 6 | import { runQuery, type Ctx } from './loop.ts'; |
| 7 | |
| 8 | export type { Ctx }; |
| 9 | |
| 10 | /** |
| 11 | * Wrap a customer query function. Customer code does: |
| 12 | * |
| 13 | * import { query } from '@briven/cli/server'; |
| 14 | * export const poolStats = query(async (ctx, args) => { |
| 15 | * const pools = await ctx.db('pools').select(); |
| 16 | * return { count: pools.length }; |
| 17 | * }); |
| 18 | * |
| 19 | * The wrapper is mostly a marker — the dispatcher in __entry.ts already |
| 20 | * knows what's exported. Returning the function untouched keeps semantics |
| 21 | * symmetric with the inline executor. |
| 22 | */ |
| 23 | export function query<A, R>( |
| 24 | fn: (ctx: Ctx, args: A) => Promise<R> | R, |
| 25 | ): (ctx: Ctx, args: A) => Promise<R> | R { |
| 26 | return fn; |
| 27 | } |
| 28 | |
| 29 | /** |
| 30 | * Wrap a customer mutation (write) function. Identical wiring to `query` — |
| 31 | * the `ctx.db(...)` builders already emit INSERT / UPDATE / DELETE frames to |
| 32 | * the host, which runs them inside the project's writable transaction |
| 33 | * (query-builder.ts → tx.unsafe). So a mutation needs no extra plumbing; the |
| 34 | * wrapper is a marker that mirrors the customer CLI's `@briven/cli/server` |
| 35 | * surface so a function written against the CLI runs unchanged here. |
| 36 | */ |
| 37 | export function mutation<A, R>( |
| 38 | fn: (ctx: Ctx, args: A) => Promise<R> | R, |
| 39 | ): (ctx: Ctx, args: A) => Promise<R> | R { |
| 40 | return fn; |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Wrap a customer action function — general compute with no implicit DB |
| 45 | * transaction semantics. It still receives the same `ctx` (so `ctx.db`, |
| 46 | * `ctx.env`, `ctx.log` all work), and any network it does is governed by the |
| 47 | * isolate's Deno `--allow-net` / `--deny-net` flags and the fetch shim in |
| 48 | * loop.ts. Identity wrapper, matching the customer CLI's `action`. |
| 49 | */ |
| 50 | export function action<A, R>( |
| 51 | fn: (ctx: Ctx, args: A) => Promise<R> | R, |
| 52 | ): (ctx: Ctx, args: A) => Promise<R> | R { |
| 53 | return fn; |
| 54 | } |
| 55 | |
| 56 | // --------------------------------------------------------------------------- |
| 57 | // ulid — pure prefixed-ULID string generator. |
| 58 | // |
| 59 | // Mirrors the customer CLI's `ulid` (and @briven/shared's prefixed-id shape, |
| 60 | // e.g. `newId('td')` → "td_01HZ..."). Scaffolded functions call `ulid('td')` |
| 61 | // to mint primary keys, so the runtime must provide it from |
| 62 | // `@briven/cli/server`. Pure — no host round-trip, no imports (loop.ts is |
| 63 | // resolved by Deno with no node_modules), so we inline a Crockford-base32 |
| 64 | // ULID here instead of importing the `ulid` npm package. |
| 65 | // --------------------------------------------------------------------------- |
| 66 | |
| 67 | const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // no I, L, O, U |
| 68 | const TIME_LEN = 10; |
| 69 | const RANDOM_LEN = 16; |
| 70 | |
| 71 | function encodeTime(now: number): string { |
| 72 | let out = ''; |
| 73 | let n = now; |
| 74 | for (let i = TIME_LEN - 1; i >= 0; i--) { |
| 75 | const mod = n % 32; |
| 76 | out = CROCKFORD[mod] + out; |
| 77 | n = (n - mod) / 32; |
| 78 | } |
| 79 | return out; |
| 80 | } |
| 81 | |
| 82 | function encodeRandom(): string { |
| 83 | const bytes = new Uint8Array(RANDOM_LEN); |
| 84 | crypto.getRandomValues(bytes); |
| 85 | let out = ''; |
| 86 | for (let i = 0; i < RANDOM_LEN; i++) { |
| 87 | out += CROCKFORD[bytes[i]! % 32]; |
| 88 | } |
| 89 | return out; |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Generate a ULID. With a prefix, returns `<prefix>_<ULID>` (e.g. |
| 94 | * `ulid('td')` → "td_01HZ5E4..."); without one, returns the bare 26-char |
| 95 | * ULID. Monotonic-within-ms ordering is NOT guaranteed (fresh randomness per |
| 96 | * call) — fine for primary keys, which only need uniqueness. |
| 97 | */ |
| 98 | export function ulid(prefix?: string): string { |
| 99 | const id = encodeTime(Date.now()) + encodeRandom(); |
| 100 | return prefix ? `${prefix}_${id}` : id; |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Customer-facing error base, mirrored from `@briven/shared` so functions can |
| 105 | * `import { brivenError } from '@briven/cli/server'` INSIDE the isolate — which |
| 106 | * has no node_modules to resolve the real package. Keep the shape in lockstep |
| 107 | * with packages/shared/src/errors.ts. |
| 108 | */ |
| 109 | export class brivenError extends Error { |
| 110 | readonly code: string; |
| 111 | readonly status: number; |
| 112 | readonly cause?: unknown; |
| 113 | readonly context?: Readonly<Record<string, unknown>>; |
| 114 | |
| 115 | constructor( |
| 116 | code: string, |
| 117 | message: string, |
| 118 | options: { status?: number; cause?: unknown; context?: Record<string, unknown> } = {}, |
| 119 | ) { |
| 120 | super(message); |
| 121 | this.name = 'brivenError'; |
| 122 | this.code = code; |
| 123 | this.status = options.status ?? 500; |
| 124 | this.cause = options.cause; |
| 125 | this.context = options.context ? Object.freeze({ ...options.context }) : undefined; |
| 126 | } |
| 127 | |
| 128 | toJSON(): { code: string; message: string; status: number } { |
| 129 | return { code: this.code, message: this.message, status: this.status }; |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | // Full runtime surface exposed to deployed functions via `@briven/cli/server`: |
| 134 | // query · mutation · action · ulid · brivenError (+ low-level runQuery). |
| 135 | export { runQuery }; |