@brust/core 2.0.0-alpha.3 → 2.0.0-alpha.30
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 +66 -6
- package/native/index.d.ts +63 -4
- package/native/index.js +57 -54
- package/package.json +29 -15
- package/src/actions/client.ts +53 -7
- package/src/actions/elysia.ts +1 -1
- package/src/actions/load.ts +24 -1
- package/src/actions/prefix.ts +24 -2
- package/src/actions-bridge.ts +2 -2
- package/src/assets.d.ts +55 -0
- package/src/browser.ts +6 -1
- package/src/build/actions-only.ts +86 -0
- package/src/build/build-eval.ts +332 -0
- package/src/build/build-icons.ts +69 -0
- package/src/build/bundle.ts +466 -54
- package/src/build/cf-assets.ts +4 -2
- package/src/build/cf-entry.ts +7 -3
- package/src/build/cf-wrangler.ts +3 -1
- package/src/build/cloudflare.ts +21 -11
- package/src/build/compile.ts +263 -6
- package/src/build/config.ts +85 -11
- package/src/build/css.ts +353 -0
- package/src/build/esm-resolve.ts +170 -0
- package/src/build/features.ts +119 -0
- package/src/build/index.ts +210 -27
- package/src/build/manifest.ts +88 -7
- package/src/build/pkg-src.ts +514 -0
- package/src/build/recognised.ts +19 -0
- package/src/build/scan.ts +71 -20
- package/src/build/server-only.ts +62 -0
- package/src/build/ssg-render.ts +65 -0
- package/src/build/ssg.ts +146 -0
- package/src/build/static-worker.ts +91 -0
- package/src/build/static.ts +259 -0
- package/src/build/tailwind.ts +117 -0
- package/src/build/tsconfig-paths.ts +67 -0
- package/src/build-time.ts +10 -0
- package/src/cache-adapter/types.ts +1 -1
- package/src/cf.ts +4 -1
- package/src/cli.ts +40 -4
- package/src/config.ts +111 -25
- package/src/css-modules.d.ts +8 -0
- package/src/dev/bun.ts +371 -0
- package/src/dev/client.ts +92 -0
- package/src/dev/devtools-data.ts +23 -0
- package/src/dev/devtools.ts +358 -0
- package/src/dev/diag.ts +30 -0
- package/src/dev/proxy.ts +236 -0
- package/src/dev/tui.ts +144 -0
- package/src/dev/watch.ts +130 -0
- package/src/dev.ts +15 -28
- package/src/handlers.ts +115 -9
- package/src/index.ts +6 -1
- package/src/native.ts +18 -0
- package/src/navigate-browser.ts +19 -0
- package/src/navigate.ts +19 -0
- package/src/navigation.ts +91 -0
- package/src/routes.ts +31 -1
- package/src/run.ts +53 -11
- package/src/shadcn/init.ts +513 -0
- package/src/shadcn/jsonc.ts +129 -0
- package/src/shadcn/registry.ts +149 -0
- package/src/store/builtin.ts +4 -0
- package/src/worker.ts +98 -22
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The built-in `navigation` store (SPA navigation design §11): the `brust:nav:*` events as
|
|
2
|
+
// signals, so a component reads navigation state through `useStore(navigation)` like any other store. Browser-safe and
|
|
3
|
+
// React-free, like `@brust/core/store`. The name is reserved (the compiler refuses a user `defineStore('navigation')`).
|
|
4
|
+
//
|
|
5
|
+
// Server: the factory paints `idle` and the loader seeds `url` with the request's path + search (handlers.ts), so the first
|
|
6
|
+
// paint is right and hydration matches. Browser: this module listens to the four events once and `batch`-sets the signals.
|
|
7
|
+
import { batch, defineStore, signal } from '@brust/runtime-dom/store'
|
|
8
|
+
|
|
9
|
+
export type NavigationTrigger = 'link' | 'navigate' | 'popstate'
|
|
10
|
+
|
|
11
|
+
const at = (): string => (typeof location === 'undefined' ? '/' : location.pathname + location.search)
|
|
12
|
+
|
|
13
|
+
/** `new URL(href).pathname + search`, the way the store spells a page; `href` itself when it does not parse. */
|
|
14
|
+
const path = (href: unknown): string => {
|
|
15
|
+
try {
|
|
16
|
+
const u = new URL(String(href), typeof location === 'undefined' ? 'http://x' : location.href)
|
|
17
|
+
return u.pathname + u.search
|
|
18
|
+
} catch {
|
|
19
|
+
return String(href)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare const __BRUST_DEV__: boolean | undefined
|
|
24
|
+
|
|
25
|
+
// Instances this module's factory made: a record under the name that is not one belongs to someone else's defineStore.
|
|
26
|
+
const made = new WeakSet<object>()
|
|
27
|
+
|
|
28
|
+
export const navigation = defineStore('navigation', () => {
|
|
29
|
+
const store = {
|
|
30
|
+
/** `'loading'` from `brust:nav:load` until `success` / `error`. */
|
|
31
|
+
state: signal<'idle' | 'loading'>('idle'),
|
|
32
|
+
/** Path + search of the page on screen: updated at `success`, and at `error` when the page was swapped in. */
|
|
33
|
+
url: signal<string>(at()),
|
|
34
|
+
/** Path + search being loaded, else `null`. */
|
|
35
|
+
to: signal<string | null>(null),
|
|
36
|
+
/** Of the current or last navigation. */
|
|
37
|
+
trigger: signal<NavigationTrigger | null>(null),
|
|
38
|
+
/** HTTP status of the last navigation. */
|
|
39
|
+
status: signal<number | null>(null),
|
|
40
|
+
/** Message of the last failed navigation; cleared at the next `load`. */
|
|
41
|
+
error: signal<string | null>(null),
|
|
42
|
+
/** The last navigation id (0 before any). */
|
|
43
|
+
id: signal<number>(0),
|
|
44
|
+
}
|
|
45
|
+
made.add(store)
|
|
46
|
+
return store
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// One subscription per tab, whichever chunk evaluates this module first (the store registry is shared the same way).
|
|
50
|
+
const BOUND = Symbol.for('brust.navigation')
|
|
51
|
+
if (typeof document !== 'undefined' && !(globalThis as { [BOUND]?: true })[BOUND]) {
|
|
52
|
+
;(globalThis as { [BOUND]?: true })[BOUND] = true
|
|
53
|
+
// A user `defineStore('navigation', …)` outside a component is not seen by the compiler (`store-name-reserved`); it
|
|
54
|
+
// would share this record by name, so say so in dev.
|
|
55
|
+
if (typeof __BRUST_DEV__ !== 'undefined' && __BRUST_DEV__ && !made.has(navigation()))
|
|
56
|
+
console.warn("[brust] the store name 'navigation' is reserved for @brust/core/navigation, but another defineStore('navigation') made the record in use; rename yours")
|
|
57
|
+
const on = (name: string, apply: (d: Record<string, unknown>) => void) =>
|
|
58
|
+
document.addEventListener(`brust:nav:${name}`, (e) => apply((e as CustomEvent).detail ?? {}))
|
|
59
|
+
on('load', (d) =>
|
|
60
|
+
batch(() => {
|
|
61
|
+
navigation.state.set('loading')
|
|
62
|
+
navigation.to.set(path(d.url))
|
|
63
|
+
navigation.trigger.set(d.trigger as NavigationTrigger)
|
|
64
|
+
navigation.error.set(null)
|
|
65
|
+
navigation.id.set(d.id as number)
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
on('success', (d) =>
|
|
69
|
+
batch(() => {
|
|
70
|
+
navigation.state.set('idle')
|
|
71
|
+
navigation.url.set(path(d.url))
|
|
72
|
+
navigation.to.set(null)
|
|
73
|
+
navigation.trigger.set(d.trigger as NavigationTrigger)
|
|
74
|
+
navigation.status.set(d.status as number)
|
|
75
|
+
navigation.error.set(null)
|
|
76
|
+
navigation.id.set(d.id as number)
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
on('error', (d) =>
|
|
80
|
+
batch(() => {
|
|
81
|
+
navigation.state.set('idle')
|
|
82
|
+
// A swapped error page (404, 500) is on screen; a failed swap leaves the old page where it was.
|
|
83
|
+
if (d.swapped) navigation.url.set(path(d.url))
|
|
84
|
+
navigation.to.set(null)
|
|
85
|
+
navigation.trigger.set(d.trigger as NavigationTrigger)
|
|
86
|
+
navigation.status.set((d.status as number | undefined) || null) // 0: no response
|
|
87
|
+
navigation.error.set(d.error instanceof Error ? d.error.message : d.error != null ? String(d.error) : `HTTP ${d.status}`)
|
|
88
|
+
navigation.id.set(d.id as number)
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
}
|
package/src/routes.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference path="./css-modules.d.ts" />
|
|
2
|
+
/// <reference path="./assets.d.ts" />
|
|
1
3
|
// Route tree, ids and loader verdicts (spec S4, S7 step 4). M2 routes carry ONLY
|
|
2
4
|
// `path/Component/loader/cache/children`; every other 0.1.x field fails loudly (M3).
|
|
3
5
|
|
|
@@ -23,21 +25,37 @@ export interface LoaderReq {
|
|
|
23
25
|
search: Record<string, string>
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/** The response a loader shapes (F464). One per request, shared by every loader in the chain in chain order:
|
|
29
|
+
* `set` replaces, `append` adds (repeated `Set-Cookie` survive). The headers go on whatever the chain answers:
|
|
30
|
+
* the page, `notFound()`, `redirect()` and an `httpError()`. `Content-Type`, `Content-Length`,
|
|
31
|
+
* `Transfer-Encoding`, `Connection` and `Location` are the server's (a redirect's comes from `redirect()`):
|
|
32
|
+
* setting one throws in dev and is dropped with a warning in production. */
|
|
33
|
+
export interface LoaderRes {
|
|
34
|
+
headers: Headers
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export interface LoaderCtx<P = Record<string, string>> {
|
|
27
38
|
params: P
|
|
28
39
|
path: string
|
|
29
40
|
req: LoaderReq
|
|
41
|
+
res: LoaderRes
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
/** `brust build --target static` (gaps round R9.2): `params` lists the values of a dynamic route's
|
|
45
|
+
* `{name}` segments, one record per page; `false` leaves the route out of the static output. The
|
|
46
|
+
* server and Cloudflare targets ignore it. */
|
|
47
|
+
export type RouteSsgConfig<P = Record<string, string>> = { params?: () => P[] | Promise<P[]> } | false
|
|
48
|
+
|
|
32
49
|
export interface Route<P = Record<string, string>, D = unknown> {
|
|
33
50
|
path?: string
|
|
34
51
|
Component?: ComponentType<any>
|
|
35
52
|
loader?: (ctx: LoaderCtx<P>) => Promise<D> | D
|
|
36
53
|
cache?: RouteCacheConfig
|
|
54
|
+
ssg?: RouteSsgConfig<P>
|
|
37
55
|
children?: Route<any, any>[]
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
export const ALLOWED_ROUTE_FIELDS = ['path', 'Component', 'loader', 'cache', 'children'] as const
|
|
58
|
+
export const ALLOWED_ROUTE_FIELDS = ['path', 'Component', 'loader', 'cache', 'ssg', 'children'] as const
|
|
41
59
|
const ALLOWED_CACHE_FIELDS = ['ttl_seconds', 'prefix', 'bypass', 'tags'] as const
|
|
42
60
|
const M3_CACHE_FIELDS = ['key', 'key_ttl_seconds'] as const
|
|
43
61
|
|
|
@@ -61,6 +79,18 @@ function validate(node: Route, where: string): void {
|
|
|
61
79
|
if (node.path !== undefined && typeof node.path !== 'string') throw new BrustRouteError(`route ${where}: path must be a string`)
|
|
62
80
|
if (node.loader !== undefined && typeof node.loader !== 'function')
|
|
63
81
|
throw new BrustRouteError(`route ${where}: loader must be a function`)
|
|
82
|
+
if (node.ssg !== undefined) {
|
|
83
|
+
const ssg = node.ssg as unknown
|
|
84
|
+
if (ssg !== false && (typeof ssg !== 'object' || ssg === null || Array.isArray(ssg)))
|
|
85
|
+
throw new BrustRouteError(`route ${where}: ssg must be false or { params }`)
|
|
86
|
+
if (ssg !== false) {
|
|
87
|
+
for (const k of Object.keys(ssg as object))
|
|
88
|
+
if (k !== 'params') throw new BrustRouteError(`route ${where}: unknown ssg field ssg.${k} (allowed: params)`)
|
|
89
|
+
const params = (ssg as { params?: unknown }).params
|
|
90
|
+
if (params !== undefined && typeof params !== 'function') throw new BrustRouteError(`route ${where}: ssg.params must be a function`)
|
|
91
|
+
}
|
|
92
|
+
if (node.children !== undefined) throw new BrustRouteError(`route ${where}: ssg on a layout route is not supported (put it on each leaf)`)
|
|
93
|
+
}
|
|
64
94
|
if (node.cache !== undefined) {
|
|
65
95
|
if (typeof node.cache !== 'object' || node.cache === null) throw new BrustRouteError(`route ${where}: cache must be an object`)
|
|
66
96
|
for (const k of Object.keys(node.cache)) {
|
package/src/run.ts
CHANGED
|
@@ -4,45 +4,72 @@
|
|
|
4
4
|
// once (130 / 143); a signal before every worker registered exits 0 at once (nothing is being
|
|
5
5
|
// served yet, and the server's drain only starts once it accepts). A worker that dies (error,
|
|
6
6
|
// process.exit, OOM) → drain → exit 1: M2 does not respawn, the supervisor restarts the process.
|
|
7
|
+
// F165: a build whose manifest says `needs_workers: false` (no loader, job, actions app or cache
|
|
8
|
+
// adapter) spawns none; the server then serves at once and a stray worker call answers 503.
|
|
7
9
|
// Nothing here returns on its own: a started server keeps the process alive and a drained one
|
|
8
10
|
// does not end it, so every exit is an explicit `process.exit`.
|
|
9
11
|
import { existsSync, readFileSync } from 'node:fs'
|
|
10
12
|
import { join, resolve } from 'node:path'
|
|
11
|
-
import { type BrustConfig, loadConfig } from './config'
|
|
12
|
-
import { beginDrain, type StartOptions, startServer, untilReady } from './native'
|
|
13
|
-
import {
|
|
13
|
+
import { type BrustConfig, type ConfigOverrides, loadConfig } from './config'
|
|
14
|
+
import { beginDrain, localAddr, type StartOptions, startServer, untilReady } from './native'
|
|
15
|
+
import { DEFAULT_MAX_LOADER_RESPONSE_BYTES } from './worker'
|
|
14
16
|
|
|
15
17
|
export interface RunOptions {
|
|
16
18
|
distDir?: string
|
|
17
19
|
/** The routes module the workers import (`export const routes` or default). */
|
|
18
20
|
entry?: string
|
|
19
21
|
/** Overrides below env (CLI flags): see config.ts (incl. `bootTimeoutMs`, default 30000). */
|
|
20
|
-
config?:
|
|
22
|
+
config?: ConfigOverrides
|
|
23
|
+
/** Static output (gaps round L9): runs once every worker is ready, with the bound `host:port`;
|
|
24
|
+
* then the server drains and the process exits with the returned code. */
|
|
25
|
+
onReady?: (addr: string) => Promise<number>
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/** The manifest fields `run` needs (build/manifest.ts owns the full shape). */
|
|
24
29
|
export interface RunManifest {
|
|
25
30
|
actions?: { entry: string; prefix: string; maxBodyBytes: number; maxResponseBytes?: number; crossSite?: 'reject' | 'allow' } | null
|
|
31
|
+
/** L16 R16.3: `brust build --actions-only`: no page routes and no routes entry. */
|
|
32
|
+
actions_only?: boolean
|
|
33
|
+
/** L16 R16.4b: a page-only build; the actions app is deployed elsewhere (`[actions] base_url`). */
|
|
34
|
+
actions_remote?: { prefix: string } | null
|
|
26
35
|
/** `[cache]`: the server's L2 settings (the workers load the adapter itself). */
|
|
27
36
|
cache?: { adapter: string; getTimeoutMs: number; maxBodyBytes: number; namespace: string; buildId: string } | null
|
|
37
|
+
/** F165: `false` = no route can call a worker; absent (older manifests) = the pool is needed. */
|
|
38
|
+
needs_workers?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** F165 (R40.1): the pool size `run` spawns: none only when the build said no request needs one. */
|
|
42
|
+
export function workerCount(cfg: BrustConfig, m: RunManifest): number {
|
|
43
|
+
return m.needs_workers === false ? 0 : cfg.workers
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
/** Pure: the napi `StartOptions`. No actions/cors/cache → exactly the pre-actions keys. */
|
|
31
|
-
export function startOptions(cfg: BrustConfig, distDir: string, m: RunManifest, version: string): StartOptions {
|
|
47
|
+
export function startOptions(cfg: BrustConfig, distDir: string, m: RunManifest, version: string, devToken?: string): StartOptions {
|
|
32
48
|
return {
|
|
33
49
|
host: cfg.host,
|
|
34
50
|
port: cfg.port,
|
|
35
51
|
distDir,
|
|
36
|
-
workers: cfg
|
|
52
|
+
workers: workerCount(cfg, m),
|
|
37
53
|
callTimeoutMs: cfg.callTimeoutMs,
|
|
54
|
+
...(cfg.maxLoaderResponseBytes !== undefined && cfg.maxLoaderResponseBytes !== DEFAULT_MAX_LOADER_RESPONSE_BYTES ? { maxLoaderResponseBytes: cfg.maxLoaderResponseBytes } : {}),
|
|
38
55
|
workerThreads: cfg.ioThreads,
|
|
39
56
|
generator: `brust/${version}`,
|
|
40
57
|
...(m.actions ? { actions: { prefix: m.actions.prefix, maxBodyBytes: m.actions.maxBodyBytes, ...(m.actions.maxResponseBytes !== undefined ? { maxResponseBytes: m.actions.maxResponseBytes } : {}), ...(m.actions.crossSite ? { crossSite: m.actions.crossSite } : {}) } } : {}),
|
|
58
|
+
...(actionsClient(cfg, m) ? { actionsClient: actionsClient(cfg, m) } : {}),
|
|
41
59
|
...(cfg.cors ? { cors: cfg.cors } : {}),
|
|
60
|
+
...(devToken ? { devToken } : {}),
|
|
42
61
|
...(m.cache ? { l2: { adapter: m.cache.adapter, getTimeoutMs: m.cache.getTimeoutMs, maxBodyBytes: m.cache.maxBodyBytes, namespace: m.cache.namespace, buildId: m.cache.buildId } } : {}),
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
64
|
|
|
65
|
+
/** L16: what the page tells the browser `client()` (R16.1/R16.2); absent while it would only repeat the prefix script. */
|
|
66
|
+
function actionsClient(cfg: BrustConfig, m: RunManifest): NonNullable<StartOptions['actionsClient']> | undefined {
|
|
67
|
+
const credentials = cfg.actionsCredentials !== undefined && cfg.actionsCredentials !== 'same-origin' ? cfg.actionsCredentials : undefined
|
|
68
|
+
if (cfg.actionsBaseUrl === undefined && credentials === undefined && !m.actions_remote) return undefined
|
|
69
|
+
const prefix = m.actions?.prefix ?? m.actions_remote?.prefix
|
|
70
|
+
return { ...(prefix ? { prefix } : {}), ...(cfg.actionsBaseUrl !== undefined ? { baseUrl: cfg.actionsBaseUrl } : {}), ...(credentials ? { credentials } : {}) }
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
/** Every worker's response buffer, kept reachable until the process exits (see run()). */
|
|
47
74
|
const workerBuffers: SharedArrayBuffer[] = []
|
|
48
75
|
|
|
@@ -63,7 +90,8 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
63
90
|
fail(`bad manifest ${manifest}: ${(e as Error).message}`)
|
|
64
91
|
}
|
|
65
92
|
const entry = resolve(process.env.BRUST_APP_ENTRY || opts.entry || 'routes.tsx')
|
|
66
|
-
|
|
93
|
+
// R16.3: an actions-only dist has no pages, so no routes module either.
|
|
94
|
+
if (!m.actions_only && !existsSync(entry)) fail(`routes entry ${entry} not found`)
|
|
67
95
|
|
|
68
96
|
let cfg: BrustConfig
|
|
69
97
|
try {
|
|
@@ -72,9 +100,11 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
72
100
|
fail((e as Error).message)
|
|
73
101
|
}
|
|
74
102
|
|
|
103
|
+
if (m.actions_remote && cfg.actionsBaseUrl === undefined)
|
|
104
|
+
fail('this build has no local actions app: set [actions] base_url (or BRUST_ACTIONS_BASE_URL) to the deployment that serves them')
|
|
75
105
|
const version: string = (await Bun.file(join(import.meta.dir, '../package.json')).json()).version
|
|
76
106
|
try {
|
|
77
|
-
startServer(startOptions(cfg, distDir, m, version))
|
|
107
|
+
startServer(startOptions(cfg, distDir, m, version, process.env.BRUST_DEV_TOKEN || undefined))
|
|
78
108
|
} catch (e) {
|
|
79
109
|
fail((e as Error).message)
|
|
80
110
|
}
|
|
@@ -87,6 +117,8 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
87
117
|
...process.env,
|
|
88
118
|
NODE_ENV: process.env.NODE_ENV,
|
|
89
119
|
BRUST_RENDER_SLOTS: String(cfg.renderSlots),
|
|
120
|
+
BRUST_SLOT_BYTES: String(cfg.slotBytes),
|
|
121
|
+
BRUST_MAX_LOADER_RESPONSE_BYTES: String(cfg.maxLoaderResponseBytes),
|
|
90
122
|
BRUST_DIST_DIR: distDir,
|
|
91
123
|
BRUST_APP_ENTRY: entry,
|
|
92
124
|
// One cache-adapter origin per process: its workers' published events are not replayed here.
|
|
@@ -111,11 +143,12 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
111
143
|
.finally(() => exit(failed ? 1 : code))
|
|
112
144
|
}
|
|
113
145
|
|
|
146
|
+
const workers = workerCount(cfg, m)
|
|
114
147
|
const url = new URL('./worker.ts', import.meta.url)
|
|
115
|
-
for (let i = 0; i <
|
|
148
|
+
for (let i = 0; i < workers; i++) {
|
|
116
149
|
// The response buffer is owned HERE, for the process lifetime: the Rust dispatcher keeps a raw
|
|
117
150
|
// pointer into it, so it must not be freed with the worker (crates/brust-napi dispatch.rs).
|
|
118
|
-
const sab = new SharedArrayBuffer(
|
|
151
|
+
const sab = new SharedArrayBuffer(cfg.slotBytes * cfg.renderSlots)
|
|
119
152
|
workerBuffers.push(sab)
|
|
120
153
|
const w = new Worker(url, { env: { ...env, BRUST_WORKER_ID: String(i) }, workerData: { sab } } as WorkerOptions)
|
|
121
154
|
// No respawn in M2: a worker that fails to boot or dies later takes the process down loudly
|
|
@@ -157,6 +190,15 @@ export async function run(opts: RunOptions = {}): Promise<void> {
|
|
|
157
190
|
fail(`workers not ready: ${String((e as Error).message ?? e)} (BRUST_BOOT_TIMEOUT_MS=${cfg.bootTimeoutMs})`)
|
|
158
191
|
}
|
|
159
192
|
ready = true
|
|
160
|
-
console.log(`[brust] ready (${
|
|
193
|
+
console.log(`[brust] ready (${workers} worker${workers === 1 ? '' : 's'})`)
|
|
194
|
+
if (opts.onReady) {
|
|
195
|
+
let code = 1
|
|
196
|
+
try {
|
|
197
|
+
code = await opts.onReady(localAddr())
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.error(`[brust] ${(e as Error).stack ?? String(e)}`)
|
|
200
|
+
}
|
|
201
|
+
if (!draining) drainThenExit(code)
|
|
202
|
+
}
|
|
161
203
|
await new Promise<never>(() => {})
|
|
162
204
|
}
|