@ayepi/updown 0.1.0 → 0.2.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.
- package/README.md +1 -1
- package/ayepi-updown.md +412 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ This package ships dense, machine-oriented reference docs written for **AI codin
|
|
|
66
66
|
|
|
67
67
|
- [`ayepi-updown.md`](./ayepi-updown.md)
|
|
68
68
|
|
|
69
|
-
They
|
|
69
|
+
They ship with this package and also live in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/updown).
|
|
70
70
|
|
|
71
71
|
## License
|
|
72
72
|
|
package/ayepi-updown.md
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ayepi-updown.md — reference for `@ayepi/updown`, written for coding agents.
|
|
3
|
+
|
|
4
|
+
Copy this file into any project that depends on `@ayepi/updown` (e.g. into your repo's
|
|
5
|
+
`docs/` or `.claude/` directory) and reference it from your agents and slash commands.
|
|
6
|
+
It documents the public API, the patterns the package expects, and how it works under the
|
|
7
|
+
hood, with copy-pasteable examples. Keep it in sync with the installed package version.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# `@ayepi/updown`
|
|
11
|
+
|
|
12
|
+
Graceful **startup and shutdown** orchestration. You register named **components** (a db,
|
|
13
|
+
an HTTP server, a broker, a work engine) with optional `up`/`pre`/`post` hooks and
|
|
14
|
+
`deps`. `up()` starts them in dependency order (independent ones in parallel); `down()` —
|
|
15
|
+
or a process signal — tears them down in reverse through a two-phase **pre → post**
|
|
16
|
+
shutdown. It also exposes `isLive()` / `isReady()` for Kubernetes-style liveness/readiness
|
|
17
|
+
probes. Zero dependencies, works in any runtime (Node, Deno, Bun, the browser). Reach for
|
|
18
|
+
it when you have more than one resource whose order of startup/shutdown matters and you
|
|
19
|
+
want clean draining on `SIGTERM`.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @ayepi/updown
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Mental model
|
|
26
|
+
|
|
27
|
+
- A **component** is `{ name, deps?, up?, pre?, post? }`. Every hook is optional.
|
|
28
|
+
- **Startup** (`up()`): runs each component's `up` in **dependency order** — a component
|
|
29
|
+
starts only after every name in its `deps` is `up`. Independent components run in
|
|
30
|
+
parallel.
|
|
31
|
+
- **Shutdown** (`down()`): two global phases, each in **reverse-dependency order**
|
|
32
|
+
(dependents tear down before the things they depend on):
|
|
33
|
+
1. **pre** — drain. Stop accepting new work, let in-flight work finish. `isReady()`
|
|
34
|
+
stays `true` during this phase.
|
|
35
|
+
2. **post** — teardown. Close sockets/connections. `isReady()` is already `false`.
|
|
36
|
+
All `pre` hooks across all components complete before **any** `post` hook runs.
|
|
37
|
+
- **Liveness** (`isLive()`): up and not shutting down.
|
|
38
|
+
- **Readiness** (`isReady()`): up and not yet past the drain phase.
|
|
39
|
+
|
|
40
|
+
## Public API
|
|
41
|
+
|
|
42
|
+
Everything below is exported from the package root. (Internal helpers like `withTimeout`
|
|
43
|
+
are not exported and are omitted.)
|
|
44
|
+
|
|
45
|
+
### `updown(opts?)` — the factory
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
function updown(opts?: UpDownOptions): UpDown
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Creates an independent lifecycle controller. Call it once per process you want to manage.
|
|
52
|
+
The first time you call `up()`, it validates the dependency graph and (unless disabled)
|
|
53
|
+
wires process signal handlers.
|
|
54
|
+
|
|
55
|
+
### Default instance + bound top-level exports
|
|
56
|
+
|
|
57
|
+
The package also constructs a single shared instance at import time and re-exports its
|
|
58
|
+
methods as standalone functions, for the common single-lifecycle case:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const instance = updown()
|
|
62
|
+
|
|
63
|
+
export const register = (component: Component): UpDown => instance.register(component)
|
|
64
|
+
export const up = (): Promise<void> => instance.up()
|
|
65
|
+
export const down = (): Promise<void> => instance.down()
|
|
66
|
+
export const whenDown = (): Promise<void> => instance.whenDown()
|
|
67
|
+
export const isReady = (): boolean => instance.isReady()
|
|
68
|
+
export const isLive = (): boolean => instance.isLive()
|
|
69
|
+
export const list = (): ComponentStatus[] => instance.list()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
So you can either own an instance (`const lc = updown(...)`) or use the bound globals:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { register, up, isLive, isReady } from '@ayepi/updown'
|
|
76
|
+
|
|
77
|
+
register({ name: 'db', up: () => db.connect(), post: () => db.end() })
|
|
78
|
+
await up()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The default instance is created with **default options** — signals (`SIGTERM`, `SIGINT`)
|
|
82
|
+
are wired and `exit: true` applies. If you need custom options (timeout, custom signals,
|
|
83
|
+
injected process), create your own instance with `updown(opts)` instead of using the bound
|
|
84
|
+
exports.
|
|
85
|
+
|
|
86
|
+
### `Component`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
interface Component {
|
|
90
|
+
/** Unique name. */
|
|
91
|
+
readonly name: string
|
|
92
|
+
/** Names of components that must be **up** before this one starts (shutdown runs in reverse). */
|
|
93
|
+
readonly deps?: readonly string[]
|
|
94
|
+
/** Startup work — `up()` awaits this. */
|
|
95
|
+
readonly up?: () => MaybePromise<void>
|
|
96
|
+
/** Pre-shutdown hook (the drain phase: stop accepting work, finish in-flight). */
|
|
97
|
+
readonly pre?: () => MaybePromise<void>
|
|
98
|
+
/** Post-shutdown hook (the teardown phase: close resources). */
|
|
99
|
+
readonly post?: () => MaybePromise<void>
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
All hooks may be sync or async (`MaybePromise<void> = void | Promise<void>`). A component
|
|
104
|
+
with no hooks at all is legal — it's just an ordering node / a thing to report in `list()`.
|
|
105
|
+
|
|
106
|
+
### `UpDownOptions`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
interface UpDownOptions {
|
|
110
|
+
/** Process signals that trigger `down()` (default `['SIGTERM', 'SIGINT']`); `false` to disable. */
|
|
111
|
+
readonly signals?: readonly Signal[] | false
|
|
112
|
+
/** Call `process.exit(0)` after a **signal-triggered** shutdown completes (default `true`). Explicit `down()` never exits. */
|
|
113
|
+
readonly exit?: boolean
|
|
114
|
+
/** Bound `up()` and `down()` each to this many milliseconds (0 / omitted = no timeout). */
|
|
115
|
+
readonly timeout?: number
|
|
116
|
+
/** Called when a component hook throws (shutdown is best-effort and continues). */
|
|
117
|
+
readonly onError?: (error: unknown, phase: Phase, name: string) => void
|
|
118
|
+
/** Override the process object signals attach to (defaults to the global `process`). */
|
|
119
|
+
readonly process?: ProcessLike
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `signals` — which signals trigger `down()`. `false` disables signal handling entirely
|
|
124
|
+
(useful in tests and libraries). Default `['SIGTERM', 'SIGINT']`.
|
|
125
|
+
- `exit` — only affects **signal-triggered** shutdowns. After such a shutdown completes,
|
|
126
|
+
`process.exit(0)` is called unless `exit: false`. An explicit `down()` call **never**
|
|
127
|
+
exits the process.
|
|
128
|
+
- `timeout` — bounds **both** `up()` and `down()`. If `up()` exceeds it, `up()` rejects
|
|
129
|
+
with `updown: up() timed out after <ms>ms`. If `down()` exceeds it, `down()` still
|
|
130
|
+
resolves but reports a timeout error to `onError` with phase `'post'` and name `'*'`.
|
|
131
|
+
- `onError(error, phase, name)` — called for every hook that throws and for a `down()`
|
|
132
|
+
timeout. `phase` is `'up' | 'pre' | 'post'`.
|
|
133
|
+
- `process` — inject a `ProcessLike` to attach signal handlers to (instead of the global
|
|
134
|
+
`process`). Mainly for tests.
|
|
135
|
+
|
|
136
|
+
### `UpDown` — the controller interface
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
interface UpDown {
|
|
140
|
+
/** Register a component. Chainable. Throws after `up()` has started or on a duplicate name. */
|
|
141
|
+
register(component: Component): UpDown
|
|
142
|
+
/** Start all components in dependency order. Idempotent (returns the same promise). Rejects if any `up` throws. */
|
|
143
|
+
up(): Promise<void>
|
|
144
|
+
/** Run the pre then post shutdown phases in reverse-dependency order. Idempotent. Always resolves (best-effort). */
|
|
145
|
+
down(): Promise<void>
|
|
146
|
+
/** Resolve when shutdown has completed — **without** triggering it (await a signal-driven `down()`). */
|
|
147
|
+
whenDown(): Promise<void>
|
|
148
|
+
/** `true` once up completes and the pre phase has not finished (ok to serve traffic). */
|
|
149
|
+
isReady(): boolean
|
|
150
|
+
/** `true` once up completes and shutdown has not been requested. */
|
|
151
|
+
isLive(): boolean
|
|
152
|
+
/** A snapshot of every registered component and its status. */
|
|
153
|
+
list(): ComponentStatus[]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Key behaviors:
|
|
158
|
+
|
|
159
|
+
- `register` is **chainable** (returns the `UpDown`) and throws on a duplicate `name` or
|
|
160
|
+
if called after `up()` has started.
|
|
161
|
+
- `up()` is **idempotent**: repeated calls return the same promise. It rejects if any
|
|
162
|
+
`up` hook throws, marking that component `failed`.
|
|
163
|
+
- `down()` is **idempotent** and **best-effort**: it always resolves, even if hooks throw
|
|
164
|
+
or hang (with `timeout`). Throwing hooks are reported via `onError`, not rethrown.
|
|
165
|
+
- `whenDown()` resolves when shutdown finishes but does **not** trigger it — use it to
|
|
166
|
+
block `main()` until a signal arrives.
|
|
167
|
+
|
|
168
|
+
### Supporting types
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
/** A process signal name (e.g. `'SIGTERM'`). */
|
|
172
|
+
type Signal = 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'SIGUSR2' | (string & {})
|
|
173
|
+
|
|
174
|
+
/** A component's current lifecycle status. */
|
|
175
|
+
type Status = 'idle' | 'starting' | 'up' | 'pre' | 'post' | 'down' | 'failed'
|
|
176
|
+
|
|
177
|
+
/** The shutdown phase a hook error occurred in. */
|
|
178
|
+
type Phase = 'up' | 'pre' | 'post'
|
|
179
|
+
|
|
180
|
+
/** A component's name, deps, current Status, and last error (if any). */
|
|
181
|
+
interface ComponentStatus {
|
|
182
|
+
readonly name: string
|
|
183
|
+
readonly deps: readonly string[]
|
|
184
|
+
readonly status: Status
|
|
185
|
+
readonly error?: unknown
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The minimal process surface signal handling uses (the global `process`, or your own). */
|
|
189
|
+
interface ProcessLike {
|
|
190
|
+
on?(event: string, handler: () => void): void
|
|
191
|
+
off?(event: string, handler: () => void): void
|
|
192
|
+
exit?(code: number): void
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The normal status progression for a started component is
|
|
197
|
+
`idle → starting → up → pre → post → down`. A failed `up`/`pre`/`post` lands it on
|
|
198
|
+
`failed`. A component that was never started (e.g. `down()` before `up()`) stays `idle`
|
|
199
|
+
and its `pre`/`post` are skipped.
|
|
200
|
+
|
|
201
|
+
## Examples
|
|
202
|
+
|
|
203
|
+
### Components with dependencies, ordered startup, graceful SIGTERM shutdown
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { updown } from '@ayepi/updown'
|
|
207
|
+
|
|
208
|
+
const lc = updown() // SIGTERM/SIGINT trigger shutdown by default; exit(0) afterward
|
|
209
|
+
|
|
210
|
+
lc.register({ name: 'db', up: () => db.connect(), post: () => db.end() })
|
|
211
|
+
lc.register({ name: 'cache', up: () => cache.connect(), post: () => cache.quit() })
|
|
212
|
+
lc.register({
|
|
213
|
+
name: 'http',
|
|
214
|
+
deps: ['db', 'cache'], // starts after db AND cache
|
|
215
|
+
up: () => server.listen(3000),
|
|
216
|
+
pre: () => server.stopAcceptingNewConnections(), // drain
|
|
217
|
+
post: () => server.close(), // teardown
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await lc.up() // db + cache (parallel), then http
|
|
221
|
+
// On SIGTERM: pre(http) → pre(db,cache) → [isReady=false] → post(http) → post(db,cache) → exit(0)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`register` is chainable, so this is equivalent:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
updown()
|
|
228
|
+
.register({ name: 'db', up: () => db.connect(), post: () => db.end() })
|
|
229
|
+
.register({ name: 'http', deps: ['db'], up: () => server.listen() })
|
|
230
|
+
.up()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Readiness / liveness wiring (HTTP health endpoints)
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const lc = updown()
|
|
237
|
+
|
|
238
|
+
lc.register({ name: 'http', up: () => server.listen() })
|
|
239
|
+
await lc.up()
|
|
240
|
+
|
|
241
|
+
app.get('/livez', (_req, res) => res.status(lc.isLive() ? 200 : 503).end())
|
|
242
|
+
app.get('/readyz', (_req, res) => res.status(lc.isReady() ? 200 : 503).end())
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
During a rolling deploy the sequence is: SIGTERM arrives → `isLive()` flips `false`
|
|
246
|
+
immediately → the `pre` (drain) phase runs, during which `isReady()` (and so `/readyz`)
|
|
247
|
+
stays `200` so in-flight requests can finish → once `pre` finishes, `isReady()` flips
|
|
248
|
+
`false` and the `post` teardown runs.
|
|
249
|
+
|
|
250
|
+
> Readiness stays `true` *through* the drain phase by design: you want to keep finishing
|
|
251
|
+
> in-flight requests. If your orchestrator should stop sending traffic the instant
|
|
252
|
+
> shutdown begins, gate routing on `isLive()` instead (it flips `false` the moment
|
|
253
|
+
> shutdown is requested).
|
|
254
|
+
|
|
255
|
+
### Block `main()` until a signal arrives
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { updown } from '@ayepi/updown'
|
|
259
|
+
|
|
260
|
+
async function main() {
|
|
261
|
+
const lc = updown()
|
|
262
|
+
lc.register({ name: 'http', up: () => server.listen() })
|
|
263
|
+
await lc.up()
|
|
264
|
+
await lc.whenDown() // resolves after SIGTERM → drain → teardown completes
|
|
265
|
+
}
|
|
266
|
+
main()
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Custom options: timeout + error logging, signals disabled
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const lc = updown({
|
|
273
|
+
signals: ['SIGTERM', 'SIGINT'], // or `false` to manage shutdown yourself
|
|
274
|
+
exit: true, // process.exit(0) after a signal shutdown
|
|
275
|
+
timeout: 30_000, // bound up()/down(); resolve even if a hook hangs
|
|
276
|
+
onError: (err, phase, name) => log.error({ err, phase, name }, 'lifecycle hook failed'),
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Integrating an ayepi server + broker + work engine
|
|
281
|
+
|
|
282
|
+
A realistic shutdown order: stop the work engine from picking up *new* jobs first
|
|
283
|
+
(`pre`), drain the HTTP server, then close the broker and engine, then disconnect Redis
|
|
284
|
+
last (everything depends on it).
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { updown } from '@ayepi/updown'
|
|
288
|
+
|
|
289
|
+
const lc = updown({ onError: (err, phase, name) => log.error({ err, phase, name }) })
|
|
290
|
+
|
|
291
|
+
// Shared transport — closed last because everything depends on it.
|
|
292
|
+
lc.register({
|
|
293
|
+
name: 'redis',
|
|
294
|
+
up: () => redis.connect(),
|
|
295
|
+
post: () => redis.quit(),
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Pub/sub broker, depends on redis.
|
|
299
|
+
lc.register({
|
|
300
|
+
name: 'broker',
|
|
301
|
+
deps: ['redis'],
|
|
302
|
+
up: () => broker.start(),
|
|
303
|
+
pre: () => broker.stopReceiving(), // stop accepting new messages
|
|
304
|
+
post: () => broker.close(), // flush + disconnect
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// @ayepi/work engine — stop claiming jobs on pre, let running jobs finish, close on post.
|
|
308
|
+
lc.register({
|
|
309
|
+
name: 'work',
|
|
310
|
+
deps: ['redis', 'broker'],
|
|
311
|
+
up: () => engine.start(),
|
|
312
|
+
pre: () => engine.drain(), // stop claiming new jobs; await in-flight
|
|
313
|
+
post: () => engine.close(),
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// @ayepi/core HTTP/WebSocket server — outermost; drains first, closes first.
|
|
317
|
+
lc.register({
|
|
318
|
+
name: 'http',
|
|
319
|
+
deps: ['redis', 'broker', 'work'],
|
|
320
|
+
up: () => server.listen(3000),
|
|
321
|
+
pre: () => server.stopAcceptingNewConnections(),
|
|
322
|
+
post: () => server.close(),
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
await lc.up()
|
|
326
|
+
// startup order: redis → broker → work → http
|
|
327
|
+
// shutdown pre: http → work → broker → redis (reverse; dependents first)
|
|
328
|
+
// shutdown post: http → work → broker → redis (after ALL pre hooks finish)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
> Hook names (`engine.drain()`, `broker.stopReceiving()`, etc.) are illustrative — wire
|
|
332
|
+
> them to whatever the corresponding `@ayepi/*` package actually exposes. The point is the
|
|
333
|
+
> shape: `up` to start, `pre` to drain, `post` to close, with `deps` declaring order.
|
|
334
|
+
|
|
335
|
+
### Inspecting status
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
lc.list()
|
|
339
|
+
// [
|
|
340
|
+
// { name: 'redis', deps: [], status: 'up' },
|
|
341
|
+
// { name: 'http', deps: ['redis','broker','work'], status: 'up' },
|
|
342
|
+
// ...
|
|
343
|
+
// ]
|
|
344
|
+
// After a failed hook the entry also carries `error`:
|
|
345
|
+
// { name: 'http', deps: [...], status: 'failed', error: Error('listen EADDRINUSE') }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## How it works under the hood
|
|
349
|
+
|
|
350
|
+
**Dependency ordering (topological).** `up()` first calls an internal `checkGraph()` that
|
|
351
|
+
does a DFS over the registered components, detecting cycles (`updown: dependency cycle:
|
|
352
|
+
a → b → a`) and unknown deps (`updown: "a" depends on unknown component "missing"`) — both
|
|
353
|
+
thrown synchronously from `up()`. Startup then walks the graph: each component's start
|
|
354
|
+
promise first `await Promise.all(deps.map(start))`, so a component's `up` runs only after
|
|
355
|
+
all its deps are `up`. Independent subtrees run concurrently. Promises are memoized per
|
|
356
|
+
component, so a shared dependency runs once.
|
|
357
|
+
|
|
358
|
+
**Two-phase shutdown.** `down()` runs `runPhase('pre')` to completion, flips `preDone`,
|
|
359
|
+
then runs `runPhase('post')`. Each phase walks the graph in **reverse**: a component waits
|
|
360
|
+
for all of its *dependents* (`await Promise.all(dependentsOf(name).map(run))`) before
|
|
361
|
+
running its own hook. That means dependents drain/close before the things they depend on,
|
|
362
|
+
in both phases. All `pre` hooks finish before any `post` hook starts. Components that are
|
|
363
|
+
`idle` (never started) or already `failed` are skipped.
|
|
364
|
+
|
|
365
|
+
**How readiness/liveness flip.** Three internal flags drive the probes:
|
|
366
|
+
|
|
367
|
+
- `downRequested` — set `true` synchronously at the very top of `down()`. `isLive()` is
|
|
368
|
+
`upDone && !downRequested`, so liveness drops the instant shutdown begins.
|
|
369
|
+
- `preDone` — set `true` after the `pre` phase completes. `isReady()` is
|
|
370
|
+
`upDone && !preDone`, so readiness stays `true` through draining and drops when `post`
|
|
371
|
+
begins.
|
|
372
|
+
- `upDone` — set `true` only after `up()` finishes. Both probes are `false` before
|
|
373
|
+
startup completes and after a failed `up()`.
|
|
374
|
+
|
|
375
|
+
**Signal handling.** On the first `up()`, `wireSignals()` registers one handler per
|
|
376
|
+
configured signal on the process object (the global `process` or an injected
|
|
377
|
+
`ProcessLike`). Each handler calls `down()` and, unless `exit: false`, `process.exit(0)`
|
|
378
|
+
when it resolves. After shutdown completes, `unwireSignals()` removes every handler via
|
|
379
|
+
`process.off`. If the process object has no `.on` method, signal wiring silently
|
|
380
|
+
no-ops (so it's safe in non-Node runtimes). Explicit `down()` calls never call
|
|
381
|
+
`process.exit`.
|
|
382
|
+
|
|
383
|
+
**Timers / `unref`.** The `timeout` option uses `setTimeout` whose handle is `unref()`'d
|
|
384
|
+
(when the runtime supports it), so a pending timeout never keeps the event loop alive on
|
|
385
|
+
its own. On a `down()` timeout, shutdown still resolves and reports
|
|
386
|
+
`updown: down() timed out after <ms>ms` to `onError` with name `'*'`; on an `up()`
|
|
387
|
+
timeout, `up()` rejects.
|
|
388
|
+
|
|
389
|
+
## Gotchas / constraints
|
|
390
|
+
|
|
391
|
+
- **Register before `up()`.** `register()` throws once `up()` has started. Wire the whole
|
|
392
|
+
graph first.
|
|
393
|
+
- **`up()` and `down()` are idempotent.** Repeat calls return the *same* promise — you
|
|
394
|
+
can call `down()` from multiple paths (signal + manual) safely.
|
|
395
|
+
- **`down()` never rejects.** It's best-effort: hook errors and timeouts go to `onError`;
|
|
396
|
+
the promise still resolves. Don't rely on a rejection to detect shutdown failure — check
|
|
397
|
+
`list()` for `failed` statuses or watch `onError`.
|
|
398
|
+
- **`up()` *does* reject** if any `up` hook throws (or on timeout), and leaves the failing
|
|
399
|
+
component `failed`. Components already started are **not** auto-rolled-back — call
|
|
400
|
+
`down()` yourself if you want to tear them down.
|
|
401
|
+
- **`exit` only applies to signal-triggered shutdowns.** A manual `await lc.down()` will
|
|
402
|
+
not exit the process; you control the exit.
|
|
403
|
+
- **Shutdown mid-startup is handled.** If `down()` is requested while `up()` is still
|
|
404
|
+
running, in-flight `up` hooks settle but components that haven't started yet are
|
|
405
|
+
**skipped** (their `up` never runs), and they won't have `pre`/`post` run either.
|
|
406
|
+
- **Default-instance exports use default options.** The bound `register`/`up`/`down`/…
|
|
407
|
+
exports are tied to one shared instance created with no options (signals on,
|
|
408
|
+
`exit: true`, no timeout). For any custom option, create your own `updown(opts)`.
|
|
409
|
+
- **`timeout` bounds the *whole* `up()`/`down()`, not per-hook.** A single slow hook can
|
|
410
|
+
consume the entire budget.
|
|
411
|
+
- **Readiness stays up during drain.** This is intentional (finish in-flight work). If you
|
|
412
|
+
want traffic cut the instant shutdown starts, gate on `isLive()` instead of `isReady()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayepi/updown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Graceful startup/shutdown orchestration — named components with dependencies, two-phase shutdown, liveness/readiness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"ayepi-*.md"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|