@ultimat3/core 24.0.0 → 25.1.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/CLAUDE.md +24 -27
- package/README.md +72 -34
- package/package.json +4 -7
- package/src/actor.ts +21 -0
- package/src/address-class.ts +40 -4
- package/src/assert.ts +9 -5
- package/src/audit.ts +144 -0
- package/src/aws-sigv4.ts +275 -0
- package/src/backoff.ts +16 -0
- package/src/bunfs.ts +17 -0
- package/src/client-dispatch.ts +24 -3
- package/src/client-flight.ts +68 -13
- package/src/client-problem.ts +62 -6
- package/src/client-retry-after.ts +47 -0
- package/src/client-transport.ts +3 -1
- package/src/client-wire.ts +27 -3
- package/src/config-ai.ts +32 -0
- package/src/config-defaults.ts +18 -11
- package/src/config-fixes.ts +0 -10
- package/src/config-health.ts +9 -2
- package/src/config-jobs.ts +51 -0
- package/src/config-keys.ts +170 -0
- package/src/config-mail.ts +73 -0
- package/src/config-merge.ts +1 -1
- package/src/config-navigation.ts +1 -25
- package/src/config-pwa.ts +42 -5
- package/src/config-removed.ts +131 -0
- package/src/config-shape.ts +0 -33
- package/src/config.ts +71 -94
- package/src/context.ts +16 -12
- package/src/cookie.ts +267 -3
- package/src/core-error-codes.ts +2 -0
- package/src/cursor-page.ts +41 -0
- package/src/cursor.ts +23 -5
- package/src/deprecation.ts +77 -0
- package/src/dev-secrets.ts +18 -7
- package/src/drain-deadline.ts +43 -0
- package/src/env-example.ts +9 -29
- package/src/errors.ts +18 -9
- package/src/exports/error-contract.ts +0 -1
- package/src/exports/observability.ts +1 -1
- package/src/exports/secrets.ts +3 -0
- package/src/finite-option.ts +1 -1
- package/src/flight-gate.ts +29 -14
- package/src/generation-fence.ts +1 -1
- package/src/ids.ts +7 -7
- package/src/image/canvas.ts +76 -5
- package/src/image/pipeline.ts +17 -5
- package/src/image/raster.ts +24 -1
- package/src/index.ts +90 -35
- package/src/iso-date.ts +1 -1
- package/src/lifecycle-errors.ts +1 -1
- package/src/lifecycle-readiness.ts +60 -2
- package/src/lifecycle-signals.ts +27 -2
- package/src/lifecycle-types.ts +96 -0
- package/src/lifecycle.ts +44 -133
- package/src/locale-direction.ts +1 -1
- package/src/logger.ts +16 -7
- package/src/mcp-exposure.ts +70 -8
- package/src/measurement-actor.ts +16 -1
- package/src/metric-errors.ts +32 -0
- package/src/metric-registry.ts +151 -0
- package/src/metric-series.ts +94 -0
- package/src/metrics.ts +9 -255
- package/src/page.ts +4 -2
- package/src/pg-executor.ts +2 -2
- package/src/registrar.ts +1 -0
- package/src/retry.ts +25 -5
- package/src/secrets-key-file.ts +139 -0
- package/src/secrets-store.ts +32 -16
- package/src/service.ts +5 -5
- package/src/single-flight.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/theme-storage.ts +12 -0
- package/src/type-pins.ts +51 -1
- package/src/image/fixtures.ts +0 -263
- package/src/time-zone-name.ts +0 -14
package/src/index.ts
CHANGED
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
// secrets — because each is one subject spread over a dozen modules. Every name they carry is
|
|
6
6
|
// still written out below: `export *` would make the contract something a reader has to resolve.
|
|
7
7
|
|
|
8
|
-
// Anchored on purpose, and
|
|
9
|
-
// the module regardless (oven-sh/bun#40650)
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
8
|
+
// Anchored on purpose, and not by the `sideEffects` array alone: Bun before 1.4.1 read any array as
|
|
9
|
+
// `false` and dropped the module regardless (oven-sh/bun#40650), and a bare import holds on every
|
|
10
|
+
// bundler — the array still lists both, because one that honours it drops a bare import of a module
|
|
11
|
+
// it does not list. This module registers @ultimat3/schema's error TITLES, because schema is tier 0
|
|
12
|
+
// and cannot register its own — and what reads them is `UltimateError`'s constructor, which never
|
|
13
|
+
// imports this file. Shaken out, every X_VALIDATION_FAILED renders untitled in the browser with
|
|
14
|
+
// nothing to say why. `SIDE_EFFECTS_ANCHORS` carries the argument and `bun run side-effects`
|
|
15
|
+
// enforces it. `context.ts`, `lifecycle-errors.ts` and `secrets-errors.ts` also run something at
|
|
16
|
+
// import and are neither anchored NOR listed: only their own bindings need the effect, so they ride
|
|
17
|
+
// along exactly where they are used. Listed, Bun 1.4.2 kept all three in every chunk reaching this
|
|
18
|
+
// barrel, ~5.4 kB an island (`SIDE_EFFECTS_BY_USE` in `scripts/side-effects.ts`).
|
|
17
19
|
import './core-error-codes';
|
|
18
20
|
import './schema-error-codes';
|
|
19
21
|
|
|
@@ -30,9 +32,11 @@ export {
|
|
|
30
32
|
ACTOR_KINDS,
|
|
31
33
|
actorFact,
|
|
32
34
|
actorLabel,
|
|
35
|
+
actorOf,
|
|
33
36
|
actorOrigin,
|
|
34
37
|
agentActor,
|
|
35
38
|
anonymousActor,
|
|
39
|
+
grantCovers,
|
|
36
40
|
hasRole,
|
|
37
41
|
hasScope,
|
|
38
42
|
isActorKind,
|
|
@@ -42,22 +46,45 @@ export {
|
|
|
42
46
|
withFacts,
|
|
43
47
|
} from './actor';
|
|
44
48
|
export type { AddressClass } from './address-class';
|
|
45
|
-
export { classifyAddress, isPublicAddress } from './address-class';
|
|
49
|
+
export { addressNetwork, classifyAddress, isPublicAddress } from './address-class';
|
|
46
50
|
export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
|
|
47
|
-
export {
|
|
51
|
+
export { type AssertCodedOptions, assert, assertCoded, assertNever } from './assert';
|
|
48
52
|
export { type AsyncContext, asyncContext } from './async-context';
|
|
49
53
|
/** The four shapes an async region can be in — produced by `realtime`, rendered by `ui`. */
|
|
50
54
|
export type { AsyncState } from './async-state';
|
|
55
|
+
/** The audit seam `action` and `query` share: one record shape, one installed sink. */
|
|
56
|
+
export type {
|
|
57
|
+
AuditFailure,
|
|
58
|
+
AuditOutcome,
|
|
59
|
+
AuditPrimitive,
|
|
60
|
+
AuditRecord,
|
|
61
|
+
AuditSink,
|
|
62
|
+
AuditSurface,
|
|
63
|
+
} from './audit';
|
|
64
|
+
export {
|
|
65
|
+
AUDIT_RECORD_FIELDS,
|
|
66
|
+
getAuditSink,
|
|
67
|
+
resetAuditSink,
|
|
68
|
+
setAuditSink,
|
|
69
|
+
} from './audit';
|
|
70
|
+
export type {
|
|
71
|
+
AwsCredentials,
|
|
72
|
+
AwsPayload,
|
|
73
|
+
SignAwsRequestInput,
|
|
74
|
+
SignedAwsRequest,
|
|
75
|
+
} from './aws-sigv4';
|
|
76
|
+
export { signAwsRequest, UNSIGNED_PAYLOAD } from './aws-sigv4';
|
|
51
77
|
export type { BackoffCurve, BackoffOptions, JitterMode, Random } from './backoff';
|
|
52
|
-
export { backoffDelay } from './backoff';
|
|
78
|
+
export { backoffDelay, jitterStatedDelay } from './backoff';
|
|
79
|
+
export { isCompiledBundle } from './bunfs';
|
|
53
80
|
export { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
|
|
54
81
|
export { canonicalJson, fingerprint } from './canonical-json';
|
|
55
82
|
export type { FetchLike, TransportRequest } from './client-dispatch';
|
|
56
|
-
export { IDEMPOTENCY_HEADER } from './client-dispatch';
|
|
83
|
+
export { BUILD_ID_HEADER, IDEMPOTENCY_HEADER } from './client-dispatch';
|
|
57
84
|
/**
|
|
58
85
|
* Flight control for a typed client, and OPT-IN by construction: `@ultimat3/action`'s and
|
|
59
86
|
* `@ultimat3/query`'s `client.ts` each name `ClientFlight` as a TYPE only, so a caller that never
|
|
60
|
-
* mentions `
|
|
87
|
+
* mentions `clientFlight` pays nothing for the fence, the dedup map or the retry loop.
|
|
61
88
|
* Both packages re-export these names unchanged; this is the one copy.
|
|
62
89
|
*/
|
|
63
90
|
export type {
|
|
@@ -67,7 +94,7 @@ export type {
|
|
|
67
94
|
FlightKeyOptions,
|
|
68
95
|
FlightPlan,
|
|
69
96
|
} from './client-flight';
|
|
70
|
-
export {
|
|
97
|
+
export { clientFlight, DEFAULT_CLIENT_RETRY, isTransientFailure } from './client-flight';
|
|
71
98
|
export type { ActionPathStyle, ActionRoute } from './client-paths';
|
|
72
99
|
export {
|
|
73
100
|
ACTION_PATH_PREFIX,
|
|
@@ -80,6 +107,9 @@ export {
|
|
|
80
107
|
splitWords,
|
|
81
108
|
} from './client-paths';
|
|
82
109
|
export type { TransportFailure } from './client-problem';
|
|
110
|
+
export { MAX_REMOTE_TITLE_LENGTH, remoteTitleOf, withStatedDelay } from './client-problem';
|
|
111
|
+
/** What a decoder reads off a refusal: the stated `Retry-After`, and the body's display title. */
|
|
112
|
+
export { MAX_RETRY_AFTER_SECONDS, retryAfterSecondsOf } from './client-retry-after';
|
|
83
113
|
/**
|
|
84
114
|
* The browser seam (plan 101): ONE HTTP function, the records envelope it decodes, the per-tab
|
|
85
115
|
* page handle records land in, and the principal fence every client layer subscribes to.
|
|
@@ -93,8 +123,6 @@ export { FRAMEWORK_CODE, problemOf, retryForStatus, traceHeaders } from './clien
|
|
|
93
123
|
export { notifyClientWrite, onClientWrite } from './client-writes';
|
|
94
124
|
export { type Clock, type FrozenClock, frozenClock, systemClock } from './clock';
|
|
95
125
|
export type {
|
|
96
|
-
AiConfig,
|
|
97
|
-
AiConfigInput,
|
|
98
126
|
AppConfig,
|
|
99
127
|
AppConfigInput,
|
|
100
128
|
AppConfigOverlay,
|
|
@@ -102,7 +130,6 @@ export type {
|
|
|
102
130
|
CacheConfig,
|
|
103
131
|
DatabaseConfig,
|
|
104
132
|
JobsConfig,
|
|
105
|
-
McpConfig,
|
|
106
133
|
NotifyConfig,
|
|
107
134
|
PwaConfigInput,
|
|
108
135
|
RealtimeConfig,
|
|
@@ -111,9 +138,17 @@ export type {
|
|
|
111
138
|
ThemeMode,
|
|
112
139
|
} from './config';
|
|
113
140
|
export { defineConfig, INBOX_RETENTION_KEYS } from './config';
|
|
141
|
+
export type { AiConfig, AiConfigInput, McpConfig } from './config-ai';
|
|
114
142
|
export type { DrainConfig, HealthConfig, ReadinessMode } from './config-health';
|
|
115
143
|
export { READINESS_MODES } from './config-health';
|
|
116
144
|
export type { IslandsConfig, IslandsSection, IslandsSectionInput } from './config-islands';
|
|
145
|
+
export { JOBS_CONCURRENCY_DEFAULT, type JobsConcurrency } from './config-jobs';
|
|
146
|
+
export type {
|
|
147
|
+
MailConfig,
|
|
148
|
+
MailRetainMimeConfig,
|
|
149
|
+
MailSection,
|
|
150
|
+
MailSectionInput,
|
|
151
|
+
} from './config-mail';
|
|
117
152
|
export type {
|
|
118
153
|
NavigationConfig,
|
|
119
154
|
NavigationSection,
|
|
@@ -125,7 +160,6 @@ export type {
|
|
|
125
160
|
export {
|
|
126
161
|
DEFAULT_SPECULATION,
|
|
127
162
|
NAVIGATION_SURFACES,
|
|
128
|
-
resolveSpeculation,
|
|
129
163
|
SPECULATION_EAGERNESS,
|
|
130
164
|
} from './config-navigation';
|
|
131
165
|
export type {
|
|
@@ -138,7 +172,7 @@ export type {
|
|
|
138
172
|
PwaShortcut,
|
|
139
173
|
PwaText,
|
|
140
174
|
} from './config-pwa';
|
|
141
|
-
export { PWA_COLOR_KEYS, PWA_SCHEMES } from './config-pwa';
|
|
175
|
+
export { isSameOriginPath, PWA_COLOR_KEYS, PWA_SCHEMES } from './config-pwa';
|
|
142
176
|
export type {
|
|
143
177
|
SeoConfig,
|
|
144
178
|
SeoConfigInput,
|
|
@@ -154,7 +188,7 @@ export type { ConflictPolicy, ResolveConflictOptions, Row } from './conflict-pol
|
|
|
154
188
|
export { resolveConflict } from './conflict-policy';
|
|
155
189
|
export type { Ctx, CtxFacts, CtxInit, CtxPatch, CtxServices, ServiceBag } from './context';
|
|
156
190
|
export {
|
|
157
|
-
|
|
191
|
+
ctxOf,
|
|
158
192
|
DEFAULT_LOCALE,
|
|
159
193
|
DEFAULT_TIME_ZONE,
|
|
160
194
|
hasContext,
|
|
@@ -165,10 +199,13 @@ export {
|
|
|
165
199
|
useService,
|
|
166
200
|
withChildContext,
|
|
167
201
|
} from './context';
|
|
168
|
-
/** The one
|
|
169
|
-
export {
|
|
202
|
+
/** The one cookie codec — auth, http and i18n each parsed `Cookie:` and spelled `Set-Cookie`. */
|
|
203
|
+
export type { CookiePriority, CookieSameSite, SetCookieOptions } from './cookie';
|
|
204
|
+
export { CookieInvalidError, readCookie, serializeSetCookie } from './cookie';
|
|
170
205
|
export type { CursorPayload } from './cursor';
|
|
171
206
|
export {
|
|
207
|
+
CURSOR_SECRET_FIX,
|
|
208
|
+
CURSOR_SECRET_KEY,
|
|
172
209
|
CursorInvalidError,
|
|
173
210
|
configureCursorSigning,
|
|
174
211
|
decodeCursor,
|
|
@@ -176,9 +213,19 @@ export {
|
|
|
176
213
|
resetCursorSigning,
|
|
177
214
|
usesDevCursorSecret,
|
|
178
215
|
} from './cursor';
|
|
216
|
+
/** The ONE page shape — `nextCursor` is `null` exactly when `hasMore` is false (25.0.0). */
|
|
217
|
+
export type { Page } from './cursor-page';
|
|
218
|
+
export { pageOf } from './cursor-page';
|
|
179
219
|
export { compareDecimalText } from './decimal-order';
|
|
220
|
+
export type { Deprecation, DeprecationField, DeprecationRender } from './deprecation';
|
|
221
|
+
export { recordDeprecatedCall, renderDeprecation } from './deprecation';
|
|
180
222
|
export type { DevSecretsOptions } from './dev-secrets';
|
|
181
|
-
export {
|
|
223
|
+
export {
|
|
224
|
+
assertNoDevSecretsOutsideLocal,
|
|
225
|
+
CursorSecretDevError,
|
|
226
|
+
devSecretsRefused,
|
|
227
|
+
} from './dev-secrets';
|
|
228
|
+
export { DRAIN_DEADLINE_DEFAULT_MS, DRAIN_DEADLINE_MAX_MS } from './drain-deadline';
|
|
182
229
|
export type {
|
|
183
230
|
Env,
|
|
184
231
|
EnvBooleanVar,
|
|
@@ -196,10 +243,8 @@ export type {
|
|
|
196
243
|
export { checkEnv, defineEnv, describeEnv, maskedEnvValues } from './env';
|
|
197
244
|
export type { EnvExampleOptions, EnvExampleReport } from './env-example';
|
|
198
245
|
export {
|
|
199
|
-
assertEnvExample,
|
|
200
246
|
checkEnvExample,
|
|
201
247
|
ENV_EXAMPLE_PATH,
|
|
202
|
-
EnvExampleDriftError,
|
|
203
248
|
envFileCandidates,
|
|
204
249
|
parseEnvKeys,
|
|
205
250
|
renderEnvExample,
|
|
@@ -269,7 +314,6 @@ export {
|
|
|
269
314
|
statedDelayMs,
|
|
270
315
|
stringField,
|
|
271
316
|
toUltimateError,
|
|
272
|
-
ULTIMATE_ERROR_BRAND,
|
|
273
317
|
UltimateError,
|
|
274
318
|
} from './exports/error-contract';
|
|
275
319
|
export type {
|
|
@@ -338,7 +382,6 @@ export {
|
|
|
338
382
|
configureTelemetry,
|
|
339
383
|
connections,
|
|
340
384
|
counter,
|
|
341
|
-
createLogger,
|
|
342
385
|
currentSampler,
|
|
343
386
|
currentSpan,
|
|
344
387
|
currentSpanContext,
|
|
@@ -405,6 +448,7 @@ export {
|
|
|
405
448
|
setLogStream,
|
|
406
449
|
startMetricExport,
|
|
407
450
|
startSpan,
|
|
451
|
+
structuredLogger,
|
|
408
452
|
traceparent,
|
|
409
453
|
tryOtlpEndpoint,
|
|
410
454
|
withSpan,
|
|
@@ -434,6 +478,7 @@ export {
|
|
|
434
478
|
masterKeyPath,
|
|
435
479
|
openSecrets,
|
|
436
480
|
parseMasterKey,
|
|
481
|
+
promoteStagedMasterKey,
|
|
437
482
|
readSecretsFile,
|
|
438
483
|
requireMasterKey,
|
|
439
484
|
revealOptionalSecret,
|
|
@@ -445,6 +490,7 @@ export {
|
|
|
445
490
|
SECRETS_KEY_MODE,
|
|
446
491
|
SecretsFileInvalidError,
|
|
447
492
|
SecretsFileMissingError,
|
|
493
|
+
SecretsKeyAclError,
|
|
448
494
|
SecretsKeyInvalidError,
|
|
449
495
|
SecretsKeyMismatchError,
|
|
450
496
|
SecretsKeyMissingError,
|
|
@@ -456,6 +502,7 @@ export {
|
|
|
456
502
|
secretsPath,
|
|
457
503
|
serializeSecretValues,
|
|
458
504
|
stagedMasterKeyPath,
|
|
505
|
+
stageMasterKeyFile,
|
|
459
506
|
writeMasterKeyFile,
|
|
460
507
|
writeSecretsFile,
|
|
461
508
|
} from './exports/secrets';
|
|
@@ -466,11 +513,11 @@ export type {
|
|
|
466
513
|
FlightGateOptions,
|
|
467
514
|
FlightGateState,
|
|
468
515
|
} from './flight-gate';
|
|
469
|
-
export {
|
|
516
|
+
export { flightGate, gateOverloaded } from './flight-gate';
|
|
470
517
|
export { fnv1a } from './fnv1a';
|
|
471
518
|
export { formatBytes } from './format-bytes';
|
|
472
519
|
export type { GenerationFence } from './generation-fence';
|
|
473
|
-
export {
|
|
520
|
+
export { generationFence, isSuperseded } from './generation-fence';
|
|
474
521
|
export type { PublicHealthBody } from './health-disclosure';
|
|
475
522
|
export { DEFAULT_HEALTH_DETAIL_PEERS, healthBody, healthPeerListed } from './health-disclosure';
|
|
476
523
|
export type { HostDecision, HostRule } from './host-rules';
|
|
@@ -488,8 +535,8 @@ export {
|
|
|
488
535
|
spanId,
|
|
489
536
|
traceId,
|
|
490
537
|
typedId,
|
|
491
|
-
uuid,
|
|
492
538
|
uuidTimestamp,
|
|
539
|
+
uuidV7,
|
|
493
540
|
} from './ids';
|
|
494
541
|
export type { ImageFit, ResizeSpec } from './image/canvas';
|
|
495
542
|
export { parseColor } from './image/color';
|
|
@@ -523,7 +570,7 @@ export type { ImageFormat, ImageInfo } from './image/probe';
|
|
|
523
570
|
export { IMAGE_FORMATS, IMAGE_MIME_TYPES, probeImage, sniffImageFormat } from './image/probe';
|
|
524
571
|
export type { ImageSize, Raster } from './image/raster';
|
|
525
572
|
export {
|
|
526
|
-
|
|
573
|
+
blankRaster,
|
|
527
574
|
hasAlpha,
|
|
528
575
|
MAX_IMAGE_PIXELS,
|
|
529
576
|
} from './image/raster';
|
|
@@ -585,7 +632,7 @@ export {
|
|
|
585
632
|
} from './lifecycle-grace';
|
|
586
633
|
export type { ReadinessCheck, ReadinessStatus } from './lifecycle-readiness';
|
|
587
634
|
export type { SignalHandlerOptions } from './lifecycle-signals';
|
|
588
|
-
export { installSignalHandlers } from './lifecycle-signals';
|
|
635
|
+
export { drainSignals, installSignalHandlers } from './lifecycle-signals';
|
|
589
636
|
export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners';
|
|
590
637
|
export type { Direction } from './locale-direction';
|
|
591
638
|
export { directionOf, isRtl } from './locale-direction';
|
|
@@ -594,9 +641,16 @@ export { localeSegment, localizePath, splitLocalePath } from './locale-path';
|
|
|
594
641
|
// The process logger's test seam, beside nothing it groups with: where a default-writer line goes.
|
|
595
642
|
export type { LogSink } from './logger';
|
|
596
643
|
export { setLogSink } from './logger';
|
|
597
|
-
export {
|
|
644
|
+
export {
|
|
645
|
+
isMcpExposed,
|
|
646
|
+
type McpAnnotationHints,
|
|
647
|
+
type McpExposureDeclaration,
|
|
648
|
+
type McpListFilterOp,
|
|
649
|
+
type McpListParams,
|
|
650
|
+
} from './mcp-exposure';
|
|
598
651
|
export type { MeasurementActorFactory } from './measurement-actor';
|
|
599
652
|
export {
|
|
653
|
+
declaredMeasurementActor,
|
|
600
654
|
defineMeasurementActor,
|
|
601
655
|
MEASUREMENT_ACTOR_ID,
|
|
602
656
|
measurementActor,
|
|
@@ -686,10 +740,11 @@ export {
|
|
|
686
740
|
type ServiceFactory,
|
|
687
741
|
} from './service';
|
|
688
742
|
export type { FlightJoin, Scheduler, SingleFlight, SingleFlightOptions } from './single-flight';
|
|
689
|
-
export {
|
|
743
|
+
export { singleFlight } from './single-flight';
|
|
690
744
|
export { endOfLiteral, maskLiterals, QUOTES, stripComments } from './source-mask';
|
|
691
745
|
export type { StoreMode } from './store-mode';
|
|
692
746
|
export { STORE_MODES, storeMode } from './store-mode';
|
|
747
|
+
export { THEME_STORAGE_KEY } from './theme-storage';
|
|
693
748
|
export { timingSafeEqual } from './timing-safe-equal';
|
|
694
749
|
export {
|
|
695
750
|
frameworkVersion,
|
package/src/iso-date.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Single responsibility: re-export `@ultimat3/schema`'s ISO date-time predicate, so a package that
|
|
2
2
|
// depends on core and not on schema — `@ultimat3/ui` formats dates, it validates nothing — judges a
|
|
3
|
-
// date string with the ONE rule `t.date` and `timestamp()` use.
|
|
3
|
+
// date string with the ONE rule `t.date` and `timestamp()` use.
|
|
4
4
|
|
|
5
5
|
export { isIsoDateTime } from '@ultimat3/schema';
|
package/src/lifecycle-errors.ts
CHANGED
|
@@ -15,7 +15,7 @@ registerErrorCodes({
|
|
|
15
15
|
*
|
|
16
16
|
* Loud rather than tolerant, because the silent version was measured and is worse than a failed
|
|
17
17
|
* boot: `state` never leaves `stopped` and `drain()` has memoized its promise, so a second
|
|
18
|
-
* `
|
|
18
|
+
* `httpServer().start()` bound a real port, answered `X_DRAINING` (503) to every request, and was
|
|
19
19
|
* still accepting connections after its own `stop()` returned — a dead listener holding a port,
|
|
20
20
|
* with no log line naming what happened.
|
|
21
21
|
*
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
// Single responsibility: the readiness CHECK — its signature
|
|
2
|
-
// decides what a failing one does to `/readyz` is
|
|
1
|
+
// Single responsibility: the readiness CHECK — its signature, its two answers, and the registry of
|
|
2
|
+
// named checks `/readyz` runs. The mode that decides what a failing one does to `/readyz` is
|
|
3
|
+
// `config-health.ts`'s; the lifecycle that reports through them is `lifecycle.ts`.
|
|
4
|
+
|
|
5
|
+
import { UltimateError } from './errors';
|
|
3
6
|
|
|
4
7
|
export type ReadinessStatus = 'ok' | 'failing';
|
|
5
8
|
|
|
@@ -19,3 +22,58 @@ export type ReadinessStatus = 'ok' | 'failing';
|
|
|
19
22
|
* is `failing`.
|
|
20
23
|
*/
|
|
21
24
|
export type ReadinessCheck = () => boolean;
|
|
25
|
+
|
|
26
|
+
const checks = new Map<string, ReadinessCheck>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a named readiness check. Returns its unregister — the same shape as `onShutdown`, and
|
|
30
|
+
* owned by whoever can be started twice, for the same reason.
|
|
31
|
+
*/
|
|
32
|
+
export function registerReadinessCheck(name: string, check: ReadinessCheck): () => void {
|
|
33
|
+
if (checks.has(name)) {
|
|
34
|
+
throw new UltimateError({
|
|
35
|
+
code: 'X_READINESS_CHECK_DUPLICATE',
|
|
36
|
+
cause: `a readiness check named "${name}" is already registered (have: ${[...checks.keys()].join(', ')})`,
|
|
37
|
+
fix: `name the second check for what it actually probes, e.g. registerReadinessCheck('${name}-replica', check) — or hold the unregister the first registration returned and call it first`,
|
|
38
|
+
meta: { name },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
checks.set(name, check);
|
|
42
|
+
return () => {
|
|
43
|
+
if (checks.get(name) === check) checks.delete(name);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Test-only: registered checks. A count that climbs across a start/stop cycle is a leak. */
|
|
48
|
+
export function readinessCheckCount(): number {
|
|
49
|
+
return checks.size;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Drop every check — `resetLifecycle()`'s, and nothing else's. */
|
|
53
|
+
export function clearReadinessChecks(): void {
|
|
54
|
+
checks.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every check, run now, by name. A check that throws is `failing` — never an unhandled error —
|
|
59
|
+
* and is handed to `onThrow`, which `lifecycle.ts` routes through its total `report`.
|
|
60
|
+
*
|
|
61
|
+
* Built through `Object.fromEntries`, never by assigning `results[name]`: assignment to the one
|
|
62
|
+
* name `__proto__` sets the PROTOTYPE instead of adding a key, so that check vanished from the
|
|
63
|
+
* report, `ready` was computed over an empty object — vacuously true — and a failing check
|
|
64
|
+
* answered 200. `fromEntries` defines own properties and has no such name.
|
|
65
|
+
*/
|
|
66
|
+
export function runReadinessChecks(
|
|
67
|
+
onThrow: (name: string, thrown: unknown) => void,
|
|
68
|
+
): Readonly<Record<string, ReadinessStatus>> {
|
|
69
|
+
const results: [string, ReadinessStatus][] = [];
|
|
70
|
+
for (const [name, check] of checks) {
|
|
71
|
+
try {
|
|
72
|
+
results.push([name, check() ? 'ok' : 'failing']);
|
|
73
|
+
} catch (thrown) {
|
|
74
|
+
results.push([name, 'failing']);
|
|
75
|
+
onThrow(name, thrown);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return Object.fromEntries(results);
|
|
79
|
+
}
|
package/src/lifecycle-signals.ts
CHANGED
|
@@ -7,11 +7,36 @@ export interface SignalHandlerOptions {
|
|
|
7
7
|
readonly signals?: readonly ProcessSignal[] | undefined;
|
|
8
8
|
/** Call `process.exit()` once drained. Off in tests. */
|
|
9
9
|
readonly exit?: boolean | undefined;
|
|
10
|
+
/** Whose default signal set to install — `process.platform` unless a test names one. */
|
|
11
|
+
readonly platform?: string | undefined;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const POSIX_SIGNALS: readonly ProcessSignal[] = Object.freeze(['SIGTERM', 'SIGINT']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Windows never sends SIGTERM to a console process: a service stop or a closed console window
|
|
18
|
+
* arrives as SIGHUP, and Ctrl-Break as SIGBREAK. Listening for the POSIX pair alone meant either
|
|
19
|
+
* one killed the process outright, mid-request, with no drain. SIGHUP stays off POSIX on purpose:
|
|
20
|
+
* there it is a terminal hang-up a supervised process should not read as "stop".
|
|
21
|
+
*/
|
|
22
|
+
const WINDOWS_SIGNALS: readonly ProcessSignal[] = Object.freeze([
|
|
23
|
+
'SIGTERM',
|
|
24
|
+
'SIGINT',
|
|
25
|
+
'SIGHUP',
|
|
26
|
+
'SIGBREAK',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/** The signals that start a drain on `platform`. */
|
|
30
|
+
export function drainSignals(platform: string = process.platform): readonly ProcessSignal[] {
|
|
31
|
+
return platform === 'win32' ? WINDOWS_SIGNALS : POSIX_SIGNALS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Install drain-on-signal handling — SIGTERM/SIGINT, plus SIGHUP/SIGBREAK on Windows
|
|
36
|
+
* (`drainSignals`). Returns an uninstall function.
|
|
37
|
+
*/
|
|
13
38
|
export function installSignalHandlers(options?: SignalHandlerOptions): () => void {
|
|
14
|
-
const signals
|
|
39
|
+
const signals = options?.signals ?? drainSignals(options?.platform);
|
|
15
40
|
const handlers = new Map<ProcessSignal, () => void>();
|
|
16
41
|
|
|
17
42
|
for (const signal of signals) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Single responsibility: the lifecycle's data model — its states, phases, hook and option shapes,
|
|
2
|
+
// and the health report it answers with. The mechanism that moves between them is `lifecycle.ts`,
|
|
3
|
+
// which re-exports every name here so a caller keeps one import path.
|
|
4
|
+
|
|
5
|
+
import type { Clock } from './clock';
|
|
6
|
+
import type { ReadinessMode } from './config-health';
|
|
7
|
+
import type { ReadinessStatus } from './lifecycle-readiness';
|
|
8
|
+
import type { Logger } from './logger';
|
|
9
|
+
|
|
10
|
+
export type HealthState = 'starting' | 'ready' | 'draining' | 'stopped';
|
|
11
|
+
|
|
12
|
+
/** Ordered. `accept` runs first, `close` last. */
|
|
13
|
+
export type ShutdownPhase = 'accept' | 'inflight' | 'close';
|
|
14
|
+
|
|
15
|
+
export const SHUTDOWN_PHASES: readonly ShutdownPhase[] = ['accept', 'inflight', 'close'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Signals Ultimate reacts to. Narrower than `NodeJS.Signals` on purpose. `SIGBREAK` is Windows'
|
|
19
|
+
* Ctrl-Break — the one console signal there that is neither Ctrl-C nor a window closing.
|
|
20
|
+
*/
|
|
21
|
+
export type ProcessSignal = 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'SIGQUIT' | 'SIGBREAK';
|
|
22
|
+
|
|
23
|
+
export interface ShutdownReason {
|
|
24
|
+
readonly signal: string;
|
|
25
|
+
/**
|
|
26
|
+
* Real monotonic ms (`systemClock`) after which hooks are abandoned — deliberately NOT the
|
|
27
|
+
* injected clock. The budget this bounds is `terminationGracePeriodSeconds`, counted by the
|
|
28
|
+
* kubelet in real seconds, so a frozen clock must be unable to extend it: read off `clock` a
|
|
29
|
+
* test that advanced an hour of fake time handed the drain a 16-minute grace period, while
|
|
30
|
+
* `waitForIdle` went on sleeping on a real `setTimeout`. `clock` still owns `uptimeMs`.
|
|
31
|
+
*/
|
|
32
|
+
readonly deadlineAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ShutdownHook = (reason: ShutdownReason) => void | Promise<void>;
|
|
36
|
+
|
|
37
|
+
export interface OnShutdownOptions {
|
|
38
|
+
readonly phase?: ShutdownPhase | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface LifecycleOptions {
|
|
42
|
+
/**
|
|
43
|
+
* The whole drain's budget — the in-flight wait AND every hook, in every phase. 25s by default,
|
|
44
|
+
* and **enforced whether or not an app sets it**: `ShutdownReason.deadlineAt` was always computed
|
|
45
|
+
* and handed to every hook, so the deadline was declared by the design and only the enforcement
|
|
46
|
+
* was missing. No hook reads `deadlineAt`, which is why it has to be imposed here.
|
|
47
|
+
*
|
|
48
|
+
* The lever is a LARGER value, not the absence of one: a `worker` holding a 10-minute job wants
|
|
49
|
+
* `configureLifecycle({ deadlineMs: 600_000 })` and a `terminationGracePeriodSeconds` at least as
|
|
50
|
+
* large. Left at 25s it is abandoned and the process exits clean — the row's visibility lease
|
|
51
|
+
* lapses and another worker re-claims it, which is what at-least-once already promises. The
|
|
52
|
+
* alternative is not "the job finishes": it is the same duplicate, delivered by SIGKILL at the
|
|
53
|
+
* kubelet's grace period, with no log line naming what overran.
|
|
54
|
+
*
|
|
55
|
+
* Screened where it is assigned: a whole number of milliseconds, 0 or more. `0` is "drain now".
|
|
56
|
+
*/
|
|
57
|
+
readonly deadlineMs?: number | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* How long `/readyz` answers 503 BEFORE the `accept` phase closes the listener — the time the
|
|
60
|
+
* endpoints controller and the ingress need to stop routing here. Closing on the flip itself left
|
|
61
|
+
* endpoints pointing at a closed socket, and a POST in that window got a 502. Added to
|
|
62
|
+
* `deadlineMs`, never taken from it. Unset: `defaultReadinessGraceMs()` of the process env at
|
|
63
|
+
* drain time — 0 in development/test, 5000 everywhere else, including a process naming no env.
|
|
64
|
+
* A whole number from 0 to 60000; 0 is no grace.
|
|
65
|
+
*/
|
|
66
|
+
readonly readinessGraceMs?: number | undefined;
|
|
67
|
+
/** What a failing check does to `/readyz` — see `ReadinessMode`. Default `'dependencies'`. */
|
|
68
|
+
readonly readiness?: ReadinessMode | undefined;
|
|
69
|
+
readonly clock?: Clock | undefined;
|
|
70
|
+
readonly logger?: Logger | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HealthReport {
|
|
74
|
+
readonly state: HealthState;
|
|
75
|
+
readonly ready: boolean;
|
|
76
|
+
readonly uptimeMs: number;
|
|
77
|
+
readonly inflight: number;
|
|
78
|
+
readonly buildId: string;
|
|
79
|
+
/** Named, because "alert on check failures BY CHECK NAME" is not writable against a boolean. */
|
|
80
|
+
readonly checks: Readonly<Record<string, ReadinessStatus>>;
|
|
81
|
+
/**
|
|
82
|
+
* How many checks are registered. `checks: {}` reads identically for "every check passed" and
|
|
83
|
+
* "nobody registered one", and only the second is a `/readyz` that means no more than "the
|
|
84
|
+
* socket is bound" — which is what the chart's and compose's healthchecks route traffic on.
|
|
85
|
+
* Reported rather than enforced: an empty registry is still ready, so a role that genuinely has
|
|
86
|
+
* no dependency does not have to invent a check to boot.
|
|
87
|
+
*/
|
|
88
|
+
readonly registered: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface HealthPayload {
|
|
92
|
+
readonly ok: boolean;
|
|
93
|
+
/** The status code the HTTP layer should return. Core stays HTTP-free; this is just data. */
|
|
94
|
+
readonly status: number;
|
|
95
|
+
readonly body: HealthReport;
|
|
96
|
+
}
|