@owlmeans/server-app 0.1.18-rc.27 → 0.1.18-rc.28

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,93 +1,306 @@
1
1
  # @owlmeans/server-app
2
2
 
3
- The main entry point for OwlMeans backend services — aggregates server packages and provides the `makeContext` + `main` bootstrap functions.
4
-
5
- ## Overview
6
-
7
- - `makeContext(cfg)` creates a fully initialized server context with Fastify, auth, WebSocket, and static resource support
8
- - `main(ctx, entrypoints)` registers entrypoints, initializes the context, and starts the HTTP server
9
- - Re-exports the most commonly used symbols from lower-level packages so backend code has a single import target
10
- - Used by every service in the viable monorepo
3
+ The bootstrap package for an OwlMeans backend process. An application uses it in every HTTP or
4
+ WebSocket API service: `makeContext` builds the base server context (Fastify API server, API
5
+ client, socket service, static resources and the Ed25519 auth guard), `main` registers the
6
+ entrypoints, initializes the context and starts listening, and the package re-exports the server
7
+ vocabulary (`bind`, `handlers`, `config`, `sservice`, `route`, ...) so backend code has one import
8
+ target. Do not use it in a shared `common` package that only declares protocols (depend on
9
+ `@owlmeans/entrypoint` and `@owlmeans/route` there), in the auth manager process (use
10
+ `@owlmeans/server-auth/manager`, which has its own `makeContext` and `main`), or in a browser app
11
+ (use `@owlmeans/web-panel`).
11
12
 
12
13
  ## Installation
13
14
 
14
15
  ```bash
15
- bun add @owlmeans/server-app@^0.1.18-rc.17
16
+ bun add @owlmeans/server-app@^0.1.18-rc.27
16
17
  ```
17
18
 
19
+ ## Concepts
20
+
21
+ - **Application context** — one per process, built by the app's own factory that calls
22
+ `makeContext(cfg)` and then adds its own resources and services with idempotent `append*` mixins.
23
+ `AppConfig` / `AppContext` are the types that factory extends.
24
+ - **`customize` flag** — `makeContext(cfg, true)` skips the default in-process `AUTH_CACHE` and
25
+ `appendAuthService`, so the app can register a shared cache (Redis) and the auth guard itself.
26
+ - **Framework entrypoints** — `entrypoints` is the list of bindings every service carries: the
27
+ `@owlmeans/server-auth` dispatcher bindings and the `@owlmeans/api-config-server` config endpoint.
28
+ - **Bindings** — the app's server-local list of `bind(protocol, handler)` results, materialized from
29
+ the shared protocol tree and passed to `main`.
30
+ - **Port hold** — `holdApiPort` answers on the service port while the context initializes, so a
31
+ slow or failing boot answers 503 instead of a connection error.
32
+
18
33
  ## Usage
19
34
 
20
- Bootstrap a backend service:
35
+ ### 1. Config
36
+
37
+ `config(service, base?)` creates a backend config for the named service; `sservice` declares a
38
+ backend service route and `service` any other (frontend) one. A service with `opened: true` listens
39
+ on `0.0.0.0`, otherwise on `127.0.0.1`.
40
+
41
+ ```ts
42
+ import { AppType, config, service, sservice } from '@owlmeans/server-app'
43
+ import type { AppConfig } from '@owlmeans/server-app'
44
+ import { MY_APP_API, MY_APP_WEB, commonConfig } from 'my-app-common'
45
+
46
+ export interface Config extends AppConfig { }
47
+
48
+ const cfg = config<Config>(MY_APP_API, commonConfig as Config)
49
+
50
+ sservice({ service: MY_APP_API, internalHost: 'localhost', internalPort: 3000 }, cfg)
51
+ cfg.services[MY_APP_API].opened = true
52
+
53
+ service({ type: AppType.Frontend, service: MY_APP_WEB, host: 'localhost', port: 5173 }, cfg)
54
+
55
+ export default cfg
56
+ ```
57
+
58
+ ### 2. Context factory
59
+
60
+ The app's factory composes the base context and adds its own resources. For development or a
61
+ single replica the defaults are enough:
62
+
63
+ ```ts
64
+ import { makeContext as makeAppContext } from '@owlmeans/server-app'
65
+ import type { AppContext } from '@owlmeans/server-app'
66
+ import { appendStaticResource } from '@owlmeans/static-resource'
67
+ import type { StaticResourceAppend } from '@owlmeans/static-resource'
68
+ import type { Resource } from '@owlmeans/resource'
69
+ import type { Project } from 'my-app-common'
70
+ import type { Config } from './config.js'
71
+
72
+ export const PROJECTS = 'my-app:projects'
73
+
74
+ export interface Context<C extends Config = Config> extends AppContext<C>, StaticResourceAppend {
75
+ project: () => Resource<Project>
76
+ }
77
+
78
+ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
79
+ const context = makeAppContext<C, T>(cfg)
80
+
81
+ appendStaticResource<C, T>(context, PROJECTS)
82
+ context.project = () => context.getStaticResource<Project>(PROJECTS)
83
+
84
+ return context
85
+ }
86
+ ```
87
+
88
+ A scaled deployment passes `customize: true` and registers a shared auth cache before the guard:
89
+
90
+ ```ts
91
+ import { klusterize, makeContext as makeAppContext } from '@owlmeans/server-app'
92
+ import { appendMongo } from '@owlmeans/mongo'
93
+ import { appendRedis } from '@owlmeans/redis'
94
+ import { makeRedisResource } from '@owlmeans/redis-resource'
95
+ import { appendAuthService, AUTH_CACHE } from '@owlmeans/server-auth'
96
+ import { appendAuthIdentityResources } from '@owlmeans/server-auth-identity'
97
+
98
+ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
99
+ const context = makeAppContext<C, T>(cfg, true)
100
+ klusterize<C, T>(context)
101
+ appendMongo<C, T>(context)
102
+ appendRedis<C, T>(context)
21
103
 
22
- ```typescript
23
- import { makeContext, main, entrypoints, config, service, AppType } from '@owlmeans/server-app'
104
+ context.registerResource(makeRedisResource(AUTH_CACHE))
105
+ appendAuthService<C, T>(context)
106
+ appendAuthIdentityResources(context)
24
107
 
25
- const appConfig = config(
26
- AppType.Backend,
27
- 'manager-api',
28
- service({ service: 'manager-api', host: process.env.API_HOST!, port: 3000 }),
29
- { port: 3000, dbs: [{ service: 'mongo', alias: 'mongo', host: process.env.MONGO_HOST!, port: 27017 }] }
30
- )
108
+ // app-local Mongo resource factory, same `project()` accessor as above
109
+ context.registerResource(makeProjectResource(PROJECTS))
110
+ context.project = () => context.resource(PROJECTS)
31
111
 
32
- const context = makeContext(appConfig)
33
- await main(context, [...entrypoints, ...appEntrypoints])
112
+ return context
113
+ }
34
114
  ```
35
115
 
36
- Bind a shared protocol with a handler:
116
+ ### 3. Handlers bound to shared protocols
37
117
 
38
- ```typescript
39
- import { bind } from '@owlmeans/server-entrypoint'
40
- import { handlers } from '@owlmeans/server-api'
41
- import { appEntrypoints as protocols } from 'my-app-common'
42
- import type { Context } from 'my-app-backend'
118
+ Protocols live in the shared package; guards and gates on a parent are inherited by its children.
119
+
120
+ ```ts
121
+ // my-app-common/src/protocols.ts
122
+ import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint'
123
+ import { route, RouteMethod, socket } from '@owlmeans/route'
124
+ import { DEFAULT_GUARD } from '@owlmeans/auth-common'
125
+
126
+ const projectBase = openProtocol(route('my-app:api:project', '/project'), {
127
+ guards: DEFAULT_GUARD,
128
+ gate: { alias: MY_APP_GATE, params: ['my-app-project-{entity}'] },
129
+ })
130
+
131
+ export const projectProtocols = {
132
+ base: projectBase,
133
+ list: protocol(
134
+ route('my-app:api:project:list', '/', { parent: projectBase, method: RouteMethod.GET }),
135
+ contract(typed<Project[]>()),
136
+ ),
137
+ create: protocol(
138
+ route('my-app:api:project:create', '/', { parent: projectBase, method: RouteMethod.POST }),
139
+ contract.request({ body: typed<CreateProject>(CreateProjectSchema) }, typed<Project>()),
140
+ ),
141
+ get: protocol(
142
+ route('my-app:api:project:get', '/:id', { parent: projectBase, method: RouteMethod.GET }),
143
+ contract.request({ params: typed<ProjectParams>(ProjectParamsSchema) }, typed<Project>()),
144
+ ),
145
+ watch: protocol(
146
+ route('my-app:api:project:watch', '/:id/watch', socket({ parent: projectBase })),
147
+ contract.request({ params: typed<ProjectParams>(ProjectParamsSchema) }, typed<void>()),
148
+ ),
149
+ }
150
+ ```
151
+
152
+ ```ts
153
+ // my-app-api/src/app/project.ts
154
+ import { handlers } from '@owlmeans/server-app'
155
+ import { requireEntityKey } from '@owlmeans/auth-common'
156
+ import { projectProtocols } from 'my-app-common'
157
+ import type { Context } from '../context.js'
43
158
 
44
159
  const api = handlers<Context>()
45
- const appEntrypoints = [
46
- bind(protocols.api.projectCreate, api.request(protocols.api.projectCreate, async (req, ctx) =>
47
- (ctx as Context).project().create(req.body))),
48
- ]
160
+
161
+ export const list = api.request(projectProtocols.list, async (request, context) => {
162
+ const { items } = await context.project().list({ entityId: requireEntityKey(request) })
163
+ return items
164
+ })
165
+
166
+ export const create = api.body(projectProtocols.create, async (body, context, request) =>
167
+ context.project().create({ ...body, entityId: requireEntityKey(request) }))
168
+
169
+ export const get = api.params(projectProtocols.get, async ({ id }, context, request) =>
170
+ context.project().load({ id, entityId: requireEntityKey(request) }))
49
171
  ```
50
172
 
51
- ## API
173
+ ### 4. Entrypoint list and `main`
52
174
 
53
- ### `makeContext<C, T>(cfg, customize?): T`
175
+ ```ts
176
+ // my-app-api/src/entrypoints.ts
177
+ import { bind, entrypoints } from '@owlmeans/server-app'
178
+ import { connection } from '@owlmeans/server-socket'
179
+ import { projectProtocols } from 'my-app-common'
180
+ import * as project from './app/project.js'
54
181
 
55
- Creates a server context with Fastify HTTP, WebSocket, static resources, and auth guard set up. Pass `customize: true` to skip the default auth setup.
182
+ export const appEntrypoints = [
183
+ ...entrypoints,
184
+ bind(projectProtocols.base),
185
+ bind(projectProtocols.list, project.list),
186
+ bind(projectProtocols.create, project.create),
187
+ bind(projectProtocols.get, project.get),
188
+ // socket protocols bind the same way; see @owlmeans/server-socket
189
+ bind(projectProtocols.watch, connection(projectProtocols.watch, async (conn, context, request) => {
190
+ await conn.notify('project:ready', { id: request.params.id })
191
+ })),
192
+ ]
56
193
 
57
- ### `main<R, C, T>(ctx, entrypoints): Promise<void>`
194
+ // my-app-api/src/index.ts
195
+ import { main } from '@owlmeans/server-app'
196
+ import config from './config.js'
197
+ import { makeContext } from './context.js'
198
+ import { appEntrypoints } from './entrypoints.js'
58
199
 
59
- Registers entrypoints, calls `configure().init()`, then starts the Fastify server.
200
+ await main(makeContext(config), appEntrypoints)
201
+ ```
60
202
 
61
- ### Re-exported symbols (for convenience)
203
+ ### 5. A boot that holds the port
62
204
 
63
- | Symbol | Source |
64
- |---|---|
65
- | `handlers` | `@owlmeans/server-api` |
66
- | `bind`, `bindAll`, `guard` | `@owlmeans/server-entrypoint` |
67
- | `route` | `@owlmeans/route` |
68
- | `broute` | `@owlmeans/server-route` |
69
- | `filter`, `body`, `params`, `EntrypointOutcome` | `@owlmeans/entrypoint` |
70
- | `config`, `service`, `sservice`, `toConfigRecord` | various config packages |
71
- | `AppType`, `BASE`, `assertContext` | `@owlmeans/context` |
72
- | `DAUTH_GUARD` | `@owlmeans/server-auth` |
73
- | `GUARD_ED25519`, `BED255_CASHE_RESOURCE` | `@owlmeans/auth-common` |
74
- | `klusterize` | `@owlmeans/kluster` |
75
- | `createListSchema`, `filterObject` | `@owlmeans/resource` |
76
- | `Request`, `Response`, `RefedEntrypointHandler` | type re-exports |
77
- | `Criteria`, `ListOptions`, `ListQuery`, `ListResult`, `Sort` | type re-exports from `@owlmeans/resource` |
205
+ `main` is `registerEntrypoints` + `configure().init()` + `getApiServer().listen()`. Write it out when
206
+ something must run between init and listen, or when the port must answer during a long boot:
78
207
 
79
- ### `entrypoints`
208
+ ```ts
209
+ import { holdApiPort } from '@owlmeans/server-app'
210
+ import type { ApiPortHold } from '@owlmeans/server-app'
80
211
 
81
- Default entrypoint array providing auth and API config routes. Spread into `main()`:
82
- ```typescript
83
- await main(context, [...entrypoints, ...myEntrypoints])
212
+ const boot = async () => {
213
+ let hold: ApiPortHold
214
+ try {
215
+ hold = await holdApiPort(config, { okPath: '/healthz', payload: () => ({ status: 'booting' }) })
216
+ } catch (error) {
217
+ console.error('port is taken', error)
218
+ process.exit(1) // a failed bind must be fatal
219
+ }
220
+
221
+ const context = makeContext(config)
222
+ context.registerEntrypoints(appEntrypoints)
223
+ try {
224
+ await context.configure().init()
225
+ await seed(context)
226
+ } catch (error) {
227
+ console.error('boot failed', error) // the hold keeps answering 503
228
+ return
229
+ }
230
+
231
+ await hold.release()
232
+ await context.getApiServer().listen()
233
+ }
234
+
235
+ await boot()
84
236
  ```
85
237
 
86
- ## Related Packages
238
+ ## API
239
+
240
+ ### Bootstrap
241
+
242
+ | Symbol | Kind | Purpose |
243
+ |---|---|---|
244
+ | `makeContext<C, T>(cfg, customize = false)` | function | Server context with API server, API client, socket service and middleware, default static resource; unless `customize`, also a static `AUTH_CACHE` and `appendAuthService` |
245
+ | `main<R, C, T>(ctx, entrypoints)` | function | Register entrypoints, `configure().init()`, then `getApiServer().listen()` |
246
+ | `entrypoints` | const | Framework bindings: `@owlmeans/server-auth` dispatcher + `@owlmeans/api-config-server` |
247
+ | `AppConfig` | type | `ServerConfig & KlusterConfig` with `services: Record<string, ServiceRoute>` |
248
+ | `AppContext<C>` | type | `ServerContext<C> & AuthServiceAppend & ApiServerAppend` |
249
+
250
+ ### Re-exports
251
+
252
+ | Symbol | Kind | Source |
253
+ |---|---|---|
254
+ | `config` | function | `@owlmeans/server-context` — `config(service, cfg?)` |
255
+ | `sservice` | function | `@owlmeans/server-config` — declare a backend service route |
256
+ | `service`, `toConfigRecord`, `PLUGINS` | function / const | `@owlmeans/config` |
257
+ | `PluginConfig` | type | `@owlmeans/config` |
258
+ | `addWebService` | function | `@owlmeans/client-config` |
259
+ | `contract`, `protocol`, `typed`, `EntrypointOutcome` | function / enum | `@owlmeans/entrypoint` |
260
+ | `Request`, `Response` | type | `AbstractRequest`, `AbstractResponse` from `@owlmeans/entrypoint` |
261
+ | `bind`, `bindAll` | function | `@owlmeans/server-entrypoint` |
262
+ | `handlers`, `holdApiPort` | function | `@owlmeans/server-api` |
263
+ | `handleBody`, `handleParams`, `handleRequest`, `handleIntermediate` | function | `@owlmeans/server-api` — unbound compatibility wrappers |
264
+ | `ApiPortHold`, `ApiPortHoldOptions` | type | `@owlmeans/server-api` |
265
+ | `route` | function | `@owlmeans/route` |
266
+ | `broute` | function | `route` from `@owlmeans/server-route` |
267
+ | `ClientEntrypoint` | type | `@owlmeans/client-entrypoint` |
268
+ | `AppType`, `BASE`, `assertContext` | enum / const / function | `@owlmeans/context` |
269
+ | `DAUTH_GUARD` | const | `DEFAULT_ALIAS` (`'auth'`) from `@owlmeans/server-auth` |
270
+ | `GUARD_ED25519`, `BED255_CASHE_RESOURCE` | const | `@owlmeans/auth-common` |
271
+ | `klusterize` | function | `@owlmeans/kluster` |
272
+ | `createListSchema`, `filterObject` | function | `@owlmeans/resource` |
273
+ | `Criteria`, `ListOptions`, `ListQuery`, `ListResult`, `Sort` | type | `@owlmeans/resource` |
274
+
275
+ ## Common pitfalls
276
+
277
+ - Keep `protocol()` declarations in the shared package; a server supplies implementations with
278
+ `bind()` and `handlers()` and never changes a declaration or finds it by alias.
279
+ - Bind parent and group declarations (`bind(projectProtocols.base)`) as well as leaves — parents
280
+ establish inherited paths, guards and gates.
281
+ - `entrypoints` holds framework registrations. Spread it into a new local array; do not mutate it.
282
+ - The default `makeContext(cfg)` backs `AUTH_CACHE` with an in-process static resource, correct for
283
+ one replica only. A scaled service passes `customize: true` and registers a Redis resource under
284
+ `AUTH_CACHE` before `appendAuthService`.
285
+ - Validation belongs in `contract()` with `typed()` or `schema()`, not in a wrapper around a handler.
286
+ Prefer `handlers<Context>()` over the unbound `handleBody` / `handleParams` / `handleRequest`.
287
+ - Key organization-scoped records on `requireEntityKey(request)` / `requireEntity(request)`; never
288
+ read an organization id from the token — `entitySlug` is the only organization value on the wire.
289
+ - Reference other services' entrypoints with `context.entrypoint(protocol)` and the protocol object,
290
+ without a caller-side response generic.
291
+ - A `holdApiPort` bind failure (`EADDRINUSE`) must end the process with a non-zero exit.
292
+
293
+ ## Related packages
87
294
 
88
- - [`@owlmeans/server-context`](../server-context) — `makeServerContext` called internally by `makeContext`
89
- - [`@owlmeans/server-api`](../server-api) — typed handler factories re-exported here
90
- - [`@owlmeans/server-entrypoint`](../server-entrypoint) — protocol binding helpers
295
+ - [`@owlmeans/server-context`](../server-context) — `makeServerContext`, called by `makeContext`
296
+ - [`@owlmeans/server-api`](../server-api) — the Fastify API server and `handlers`
297
+ - [`@owlmeans/server-entrypoint`](../server-entrypoint) — `bind` / `bindAll`
298
+ - [`@owlmeans/server-socket`](../server-socket) — WebSocket bindings
299
+ - [`@owlmeans/server-auth`](../server-auth) — the Ed25519 guard and `AUTH_CACHE`
300
+ - [`@owlmeans/server-auth-identity`](../server-auth-identity) — local identity and entity resolver
301
+ - [`@owlmeans/entrypoint`](../entrypoint) and [`@owlmeans/route`](../route) — shared protocol declarations
302
+ - [`@owlmeans/static-resource`](../static-resource) — in-memory resources
303
+ - [`@owlmeans/kluster`](../kluster) — `klusterize`
91
304
 
92
305
  <!-- owlmeans:agent-guidance:start -->
93
306
  ## Agent guidance
@@ -97,7 +310,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
97
310
  your project's skill store (`.agents/skills/`):
98
311
 
99
312
  ```sh
100
- npx @owlmeans/agent-skills@^0.1.18-rc.20
313
+ npx @owlmeans/agent-skills@^0.1.18-rc.21
101
314
  ```
102
315
 
103
316
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-app",
4
- "version": "0.1.18-rc.27",
5
- "generatedAt": "2026-09-12T14:18:54.790Z",
4
+ "version": "0.1.18-rc.28",
5
+ "generatedAt": "2026-09-15T12:21:31.923Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -6,7 +6,7 @@ description: Build OwlMeans server applications from immutable entrypoint protoc
6
6
 
7
7
  # Server application entrypoints
8
8
 
9
- **Install:** `bun add @owlmeans/server-app@^0.1.18-rc.25`
9
+ **Install:** `bun add @owlmeans/server-app@^0.1.18-rc.27`
10
10
 
11
11
  Declare routes and request/response contracts in a shared package with `protocol()` or
12
12
  `openProtocol()`. A server package supplies local implementations with `bind()` and `handlers()`;
@@ -14,15 +14,15 @@ it never changes a shared declaration by alias.
14
14
 
15
15
  ```ts
16
16
  import { bind, entrypoints, handlers } from '@owlmeans/server-app'
17
- import { projectEntrypoints } from 'project-common'
17
+ import { projectProtocols } from 'project-common'
18
18
  import { createProject } from './app/project.js'
19
19
 
20
20
  const api = handlers<Context>()
21
21
 
22
- export const appEntrypoints = [
22
+ export const serverBindings = [
23
23
  ...entrypoints,
24
- bind(projectEntrypoints.base),
25
- bind(projectEntrypoints.create, api.body(projectEntrypoints.create, createProject)),
24
+ bind(projectProtocols.base),
25
+ bind(projectProtocols.create, api.body(projectProtocols.create, createProject)),
26
26
  ]
27
27
  ```
28
28
 
@@ -0,0 +1,55 @@
1
+ import type { ApiPortHoldOptions } from '@owlmeans/server-api';
2
+ /**
3
+ * Boot phase of the process.
4
+ *
5
+ * An app binds its port before it initializes, so that a failure has somewhere to be reported
6
+ * from. Without a listener the only thing anyone sees is a connection refused at the edge — an
7
+ * opaque 503 that names neither the app nor the cause. This phase is what the health path answers
8
+ * with, and what the platform reads to tell "still starting" from "gave up, and here is why".
9
+ */
10
+ export type BootPhase = 'initializing' | 'ready' | 'failed';
11
+ export interface BootHealth extends Record<string, unknown> {
12
+ status: 'OK' | 'FAILED';
13
+ phase: BootPhase;
14
+ ok: boolean;
15
+ /** Whatever `setBootPhase` was given — the failure message, or the step being waited on. */
16
+ reason?: string;
17
+ /**
18
+ * Identity, not health: a supervisor passes the id to the child it spawns and compares it
19
+ * here, so an answer carrying another id means the port is held by a leftover process and the
20
+ * health it reports describes code nobody asked for. Empty when nothing supplied one.
21
+ */
22
+ bootId: string;
23
+ pid: number;
24
+ }
25
+ /** Env var a supervisor stamps the spawned process with; surfaced as `bootId`. */
26
+ export declare const BOOT_ID_ENV = "OWLMEANS_BOOT_ID";
27
+ export declare const getBootPhase: () => BootPhase;
28
+ export declare const getBootDetail: () => string | undefined;
29
+ /**
30
+ * `detail` is free text, and is kept for every phase — a boot that is merely slow is as worth
31
+ * explaining as one that failed.
32
+ */
33
+ export declare const setBootPhase: (phase: BootPhase, detail?: string) => void;
34
+ /**
35
+ * The body the health path answers with, from either server.
36
+ *
37
+ * The port hold and the app's own handler both build it here, so a consumer polling through the
38
+ * handover never sees the shape change under it — which is why the keys are unconditional and
39
+ * only `reason` comes and goes with the state it describes.
40
+ */
41
+ export declare const bootHealthPayload: () => BootHealth;
42
+ /**
43
+ * Hold options wired to this module — the bind-first boot in one line:
44
+ *
45
+ * const hold = await holdApiPort(ctx.cfg, bootHold(HEALTH_PATH))
46
+ * try { await init() } catch (e) { setBootPhase('failed', message); return }
47
+ * await hold.release()
48
+ * setBootPhase('ready')
49
+ * await ctx.getApiServer().listen()
50
+ *
51
+ * The hold evaluates `payload` per request, so the phase set after a failure is reported live by
52
+ * the listener that is already bound rather than needing a server that will never start.
53
+ */
54
+ export declare const bootHold: (okPath?: string) => ApiPortHoldOptions;
55
+ //# sourceMappingURL=boot-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-state.d.ts","sourceRoot":"","sources":["../src/boot-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAE9D;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE3D,MAAM,WAAW,UAAW,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzD,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAA;IACvB,KAAK,EAAE,SAAS,CAAA;IAChB,EAAE,EAAE,OAAO,CAAA;IACX,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,kFAAkF;AAClF,eAAO,MAAM,WAAW,qBAAqB,CAAA;AAK7C,eAAO,MAAM,YAAY,QAAO,SAAmB,CAAA;AAEnD,eAAO,MAAM,aAAa,QAAO,MAAM,GAAG,SAAoB,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,YAAY,UAAW,SAAS,WAAW,MAAM,KAAG,IAGhE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,QAAO,UAOnC,CAAA;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,YAAa,MAAM,KAAG,kBAGzC,CAAA"}
@@ -0,0 +1,46 @@
1
+ /** Env var a supervisor stamps the spawned process with; surfaced as `bootId`. */
2
+ export const BOOT_ID_ENV = 'OWLMEANS_BOOT_ID';
3
+ let _phase = 'initializing';
4
+ let _detail;
5
+ export const getBootPhase = () => _phase;
6
+ export const getBootDetail = () => _detail;
7
+ /**
8
+ * `detail` is free text, and is kept for every phase — a boot that is merely slow is as worth
9
+ * explaining as one that failed.
10
+ */
11
+ export const setBootPhase = (phase, detail) => {
12
+ _phase = phase;
13
+ _detail = detail;
14
+ };
15
+ /**
16
+ * The body the health path answers with, from either server.
17
+ *
18
+ * The port hold and the app's own handler both build it here, so a consumer polling through the
19
+ * handover never sees the shape change under it — which is why the keys are unconditional and
20
+ * only `reason` comes and goes with the state it describes.
21
+ */
22
+ export const bootHealthPayload = () => ({
23
+ status: _phase === 'failed' ? 'FAILED' : 'OK',
24
+ phase: _phase,
25
+ ok: _phase === 'ready',
26
+ ...(_detail != null ? { reason: _detail } : {}),
27
+ bootId: process.env[BOOT_ID_ENV] ?? '',
28
+ pid: process.pid
29
+ });
30
+ /**
31
+ * Hold options wired to this module — the bind-first boot in one line:
32
+ *
33
+ * const hold = await holdApiPort(ctx.cfg, bootHold(HEALTH_PATH))
34
+ * try { await init() } catch (e) { setBootPhase('failed', message); return }
35
+ * await hold.release()
36
+ * setBootPhase('ready')
37
+ * await ctx.getApiServer().listen()
38
+ *
39
+ * The hold evaluates `payload` per request, so the phase set after a failure is reported live by
40
+ * the listener that is already bound rather than needing a server that will never start.
41
+ */
42
+ export const bootHold = (okPath) => ({
43
+ ...(okPath != null ? { okPath } : {}),
44
+ payload: bootHealthPayload
45
+ });
46
+ //# sourceMappingURL=boot-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-state.js","sourceRoot":"","sources":["../src/boot-state.ts"],"names":[],"mappings":"AA2BA,kFAAkF;AAClF,MAAM,CAAC,MAAM,WAAW,GAAG,kBAAkB,CAAA;AAE7C,IAAI,MAAM,GAAc,cAAc,CAAA;AACtC,IAAI,OAA2B,CAAA;AAE/B,MAAM,CAAC,MAAM,YAAY,GAAG,GAAc,EAAE,CAAC,MAAM,CAAA;AAEnD,MAAM,CAAC,MAAM,aAAa,GAAG,GAAuB,EAAE,CAAC,OAAO,CAAA;AAE9D;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAgB,EAAE,MAAe,EAAQ,EAAE;IACtE,MAAM,GAAG,KAAK,CAAA;IACd,OAAO,GAAG,MAAM,CAAA;AAClB,CAAC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAe,EAAE,CAAC,CAAC;IAClD,MAAM,EAAE,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;IAC7C,KAAK,EAAE,MAAM;IACb,EAAE,EAAE,MAAM,KAAK,OAAO;IACtB,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE;IACtC,GAAG,EAAE,OAAO,CAAC,GAAG;CACjB,CAAC,CAAA;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAe,EAAsB,EAAE,CAAC,CAAC;IAChE,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrC,OAAO,EAAE,iBAAiB;CAC3B,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/server-app",
3
- "version": "0.1.18-rc.27",
3
+ "version": "0.1.18-rc.28",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,21 +21,21 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@owlmeans/api": "^0.1.18-rc.21",
25
- "@owlmeans/client-config": "^0.1.18-rc.20",
26
- "@owlmeans/client-entrypoint": "^0.1.18-rc.21",
27
- "@owlmeans/config": "^0.1.18-rc.20",
28
- "@owlmeans/context": "^0.1.18-rc.16",
29
- "@owlmeans/kluster": "^0.1.18-rc.20",
30
- "@owlmeans/entrypoint": "^0.1.18-rc.19",
31
- "@owlmeans/route": "^0.1.18-rc.17",
32
- "@owlmeans/server-api": "^0.1.18-rc.25",
33
- "@owlmeans/server-auth": "^0.1.18-rc.27",
34
- "@owlmeans/server-context": "^0.1.18-rc.20",
35
- "@owlmeans/server-entrypoint": "^0.1.18-rc.19",
36
- "@owlmeans/server-route": "^0.1.18-rc.17",
37
- "@owlmeans/server-socket": "^0.1.18-rc.27",
38
- "@owlmeans/static-resource": "^0.1.18-rc.17"
24
+ "@owlmeans/api": "^0.1.18-rc.22",
25
+ "@owlmeans/client-config": "^0.1.18-rc.21",
26
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.22",
27
+ "@owlmeans/config": "^0.1.18-rc.21",
28
+ "@owlmeans/context": "^0.1.18-rc.17",
29
+ "@owlmeans/kluster": "^0.1.18-rc.21",
30
+ "@owlmeans/entrypoint": "^0.1.18-rc.20",
31
+ "@owlmeans/route": "^0.1.18-rc.18",
32
+ "@owlmeans/server-api": "^0.1.18-rc.26",
33
+ "@owlmeans/server-auth": "^0.1.18-rc.28",
34
+ "@owlmeans/server-context": "^0.1.18-rc.21",
35
+ "@owlmeans/server-entrypoint": "^0.1.18-rc.20",
36
+ "@owlmeans/server-route": "^0.1.18-rc.18",
37
+ "@owlmeans/server-socket": "^0.1.18-rc.28",
38
+ "@owlmeans/static-resource": "^0.1.18-rc.18"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@owlmeans/dep-config": "workspace:*",