@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 +273 -60
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-app/SKILL.md +5 -5
- package/build/boot-state.d.ts +55 -0
- package/build/boot-state.d.ts.map +1 -0
- package/build/boot-state.js +46 -0
- package/build/boot-state.js.map +1 -0
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -1,93 +1,306 @@
|
|
|
1
1
|
# @owlmeans/server-app
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
104
|
+
context.registerResource(makeRedisResource(AUTH_CACHE))
|
|
105
|
+
appendAuthService<C, T>(context)
|
|
106
|
+
appendAuthIdentityResources(context)
|
|
24
107
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
112
|
+
return context
|
|
113
|
+
}
|
|
34
114
|
```
|
|
35
115
|
|
|
36
|
-
|
|
116
|
+
### 3. Handlers bound to shared protocols
|
|
37
117
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
import
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
173
|
+
### 4. Entrypoint list and `main`
|
|
52
174
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
await main(makeContext(config), appEntrypoints)
|
|
201
|
+
```
|
|
60
202
|
|
|
61
|
-
###
|
|
203
|
+
### 5. A boot that holds the port
|
|
62
204
|
|
|
63
|
-
|
|
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
|
-
|
|
208
|
+
```ts
|
|
209
|
+
import { holdApiPort } from '@owlmeans/server-app'
|
|
210
|
+
import type { ApiPortHold } from '@owlmeans/server-app'
|
|
80
211
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
##
|
|
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
|
|
89
|
-
- [`@owlmeans/server-api`](../server-api) —
|
|
90
|
-
- [`@owlmeans/server-entrypoint`](../server-entrypoint) —
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-app",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
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 {
|
|
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
|
|
22
|
+
export const serverBindings = [
|
|
23
23
|
...entrypoints,
|
|
24
|
-
bind(
|
|
25
|
-
bind(
|
|
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.
|
|
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.
|
|
25
|
-
"@owlmeans/client-config": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
27
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
28
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/kluster": "^0.1.18-rc.
|
|
30
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
31
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/server-api": "^0.1.18-rc.
|
|
33
|
-
"@owlmeans/server-auth": "^0.1.18-rc.
|
|
34
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/server-entrypoint": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/server-route": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/server-socket": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/static-resource": "^0.1.18-rc.
|
|
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:*",
|