@ultimat3/core 29.4.0 → 30.0.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 +45 -81
- package/README.md +137 -10
- package/package.json +19 -3
- package/src/address-class.ts +53 -12
- package/src/aws-sigv4.ts +104 -8
- package/src/breaker.ts +177 -0
- package/src/client-dispatch.ts +76 -17
- package/src/client-flight.ts +4 -4
- package/src/client-paths-pins.ts +92 -0
- package/src/client-problem.ts +25 -1
- package/src/client-scope.ts +3 -1
- package/src/client-stream.ts +106 -0
- package/src/client-transport.ts +16 -2
- package/src/config-assets.ts +64 -0
- package/src/config-client-errors.ts +132 -0
- package/src/config-defaults.ts +18 -4
- package/src/config-failure-capture.ts +70 -0
- package/src/config-health.ts +8 -0
- package/src/config-islands.ts +1 -1
- package/src/config-keys.ts +2 -0
- package/src/config-site.ts +16 -0
- package/src/config-time.ts +40 -0
- package/src/config.ts +37 -26
- package/src/context.ts +92 -10
- package/src/core-error-codes.ts +19 -0
- package/src/cursor.ts +7 -3
- package/src/deadline-signal.ts +57 -0
- package/src/declared-errors-openapi.ts +70 -0
- package/src/declared-errors-scan.ts +79 -0
- package/src/declared-errors.ts +185 -0
- package/src/define-error.ts +200 -0
- package/src/deprecation.ts +14 -3
- package/src/drain-deadline.ts +22 -0
- package/src/env.ts +8 -3
- package/src/error-fingerprint.ts +95 -0
- package/src/error-reporter-chain.ts +45 -0
- package/src/error-reporter-sentry.ts +4 -0
- package/src/error-reporter.ts +14 -1
- package/src/error-status-declared.ts +82 -0
- package/src/exercise-errors.ts +41 -0
- package/src/exercise-run.ts +176 -0
- package/src/exports/error-contract.ts +7 -0
- package/src/exports/observability.ts +26 -0
- package/src/failure-capture-flush.ts +93 -0
- package/src/failure-capture-mask.ts +54 -0
- package/src/failure-capture.ts +205 -0
- package/src/fill-wire.ts +30 -0
- package/src/finite-option.ts +7 -0
- package/src/health-disclosure.ts +2 -2
- package/src/hmac.ts +47 -0
- package/src/index.ts +172 -9
- package/src/json-storable.ts +21 -0
- package/src/keyed-fingerprint.ts +3 -7
- package/src/lifecycle.ts +5 -1
- package/src/logger.ts +1 -1
- package/src/otlp-metric-exporter.ts +7 -1
- package/src/otlp-post.ts +147 -0
- package/src/otlp-span-exporter.ts +7 -1
- package/src/otlp.ts +4 -37
- package/src/outbound-attempt.ts +235 -0
- package/src/outbound-dial.ts +103 -0
- package/src/outbound-errors.ts +193 -0
- package/src/outbound-hops.ts +135 -0
- package/src/outbound-read.ts +103 -0
- package/src/outbound-subpath.ts +26 -0
- package/src/outbound-types.ts +108 -0
- package/src/outbound.ts +223 -0
- package/src/page-meta.ts +8 -0
- package/src/page.ts +9 -0
- package/src/record-envelope-openapi.ts +6 -0
- package/src/record-envelope.ts +33 -9
- package/src/record-sink.ts +13 -0
- package/src/redis-client.ts +78 -0
- package/src/registrar.ts +11 -0
- package/src/render-node.ts +13 -0
- package/src/render-nodes.ts +137 -0
- package/src/retry-loop.ts +197 -0
- package/src/retry.ts +47 -145
- package/src/roles.ts +2 -2
- package/src/route-path-merge.ts +55 -0
- package/src/route-path-template.ts +124 -0
- package/src/runtime-metrics.ts +16 -1
- package/src/same-origin.ts +18 -2
- package/src/session-lost.ts +115 -0
- package/src/single-flight.ts +75 -40
- package/src/source-mask.ts +36 -2
- package/src/tool-description.ts +29 -0
- package/src/visible-interval.ts +153 -0
- package/src/webhook-signature.ts +9 -11
package/src/index.ts
CHANGED
|
@@ -46,7 +46,12 @@ export {
|
|
|
46
46
|
withFacts,
|
|
47
47
|
} from './actor';
|
|
48
48
|
export type { AddressClass } from './address-class';
|
|
49
|
-
export {
|
|
49
|
+
export {
|
|
50
|
+
addressNetwork,
|
|
51
|
+
classifyAddress,
|
|
52
|
+
isPublicAddress,
|
|
53
|
+
peerAddressClass,
|
|
54
|
+
} from './address-class';
|
|
50
55
|
/** Test support: the one name a live suite gives the database it creates (`probe-databases`). */
|
|
51
56
|
export { APP_CONFIG_FILE, APP_STATE_DIR, appDirOf, appStatePath } from './app-dir';
|
|
52
57
|
export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
|
|
@@ -72,19 +77,34 @@ export {
|
|
|
72
77
|
export type {
|
|
73
78
|
AwsCredentials,
|
|
74
79
|
AwsPayload,
|
|
80
|
+
PresignAwsUrlInput,
|
|
81
|
+
PresignedAwsUrl,
|
|
75
82
|
SignAwsRequestInput,
|
|
76
83
|
SignedAwsRequest,
|
|
77
84
|
} from './aws-sigv4';
|
|
78
|
-
export {
|
|
85
|
+
export {
|
|
86
|
+
MAX_AWS_PRESIGN_SECONDS,
|
|
87
|
+
presignAwsUrl,
|
|
88
|
+
signAwsRequest,
|
|
89
|
+
UNSIGNED_PAYLOAD,
|
|
90
|
+
} from './aws-sigv4';
|
|
79
91
|
export type { BackoffCurve, BackoffOptions, JitterMode, Random } from './backoff';
|
|
80
92
|
export { backoffDelay, jitterStatedDelay } from './backoff';
|
|
93
|
+
export type {
|
|
94
|
+
Breaker,
|
|
95
|
+
BreakerOptions,
|
|
96
|
+
BreakerState,
|
|
97
|
+
BreakerTicket,
|
|
98
|
+
BreakerVerdict,
|
|
99
|
+
} from './breaker';
|
|
100
|
+
export { breaker, settleWith } from './breaker';
|
|
81
101
|
export { isCompiledBundle } from './bunfs';
|
|
82
102
|
export type { CacheEvictionOptions } from './cache-eviction';
|
|
83
103
|
export { CACHE_TEMP_GRACE_MS, evictCacheFiles } from './cache-eviction';
|
|
84
104
|
export { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
|
|
85
105
|
export { canonicalJson, fingerprint } from './canonical-json';
|
|
86
106
|
export type { FetchLike, TransportRequest } from './client-dispatch';
|
|
87
|
-
export { BUILD_ID_HEADER, IDEMPOTENCY_HEADER } from './client-dispatch';
|
|
107
|
+
export { BUILD_ID_HEADER, IDEMPOTENCY_HEADER, REPLAYED_HEADER } from './client-dispatch';
|
|
88
108
|
/**
|
|
89
109
|
* Flight control for a typed client, and OPT-IN by construction: `@ultimat3/action`'s and
|
|
90
110
|
* `@ultimat3/query`'s `client.ts` each name `ClientFlight` as a TYPE only, so a caller that never
|
|
@@ -110,8 +130,19 @@ export {
|
|
|
110
130
|
queryPath,
|
|
111
131
|
splitWords,
|
|
112
132
|
} from './client-paths';
|
|
133
|
+
export type { ActionPin, ClientPins, PinnedActionMethod } from './client-paths-pins';
|
|
134
|
+
export {
|
|
135
|
+
encodeClientPins,
|
|
136
|
+
renderedActionPin,
|
|
137
|
+
renderedQueryPin,
|
|
138
|
+
} from './client-paths-pins';
|
|
113
139
|
export type { TransportFailure } from './client-problem';
|
|
114
|
-
export {
|
|
140
|
+
export {
|
|
141
|
+
MAX_REMOTE_TITLE_LENGTH,
|
|
142
|
+
remoteTitleOf,
|
|
143
|
+
withRequestId,
|
|
144
|
+
withStatedDelay,
|
|
145
|
+
} from './client-problem';
|
|
115
146
|
/** What a decoder reads off a refusal: the stated `Retry-After`, and the body's display title. */
|
|
116
147
|
export { MAX_RETRY_AFTER_SECONDS, retryAfterSecondsOf } from './client-retry-after';
|
|
117
148
|
/**
|
|
@@ -120,6 +151,8 @@ export { MAX_RETRY_AFTER_SECONDS, retryAfterSecondsOf } from './client-retry-aft
|
|
|
120
151
|
*/
|
|
121
152
|
export type { ClientScope } from './client-scope';
|
|
122
153
|
export { onRescope, rescope } from './client-scope';
|
|
154
|
+
export type { StreamRequest } from './client-stream';
|
|
155
|
+
export { clientStream } from './client-stream';
|
|
123
156
|
export { clientTransport } from './client-transport';
|
|
124
157
|
/** What a typed client puts on the wire. `retryForStatus` is what fills a failure's `retry`. */
|
|
125
158
|
export type { WireAnswer } from './client-wire';
|
|
@@ -143,6 +176,30 @@ export type {
|
|
|
143
176
|
} from './config';
|
|
144
177
|
export { defineConfig, INBOX_RETENTION_KEYS } from './config';
|
|
145
178
|
export type { AiConfig, AiConfigInput, McpConfig } from './config-ai';
|
|
179
|
+
export type {
|
|
180
|
+
AssetsConfig,
|
|
181
|
+
AssetsRetainConfig,
|
|
182
|
+
AssetsSection,
|
|
183
|
+
AssetsSectionInput,
|
|
184
|
+
} from './config-assets';
|
|
185
|
+
export type {
|
|
186
|
+
ClientErrorsConfig,
|
|
187
|
+
ClientErrorsSection,
|
|
188
|
+
ClientErrorsSectionInput,
|
|
189
|
+
ClientVitalsConfig,
|
|
190
|
+
ClientVitalsSurface,
|
|
191
|
+
} from './config-client-errors';
|
|
192
|
+
export {
|
|
193
|
+
CLIENT_ERRORS_DEFAULTS,
|
|
194
|
+
CLIENT_VITALS_DEFAULTS,
|
|
195
|
+
CLIENT_VITALS_SURFACES,
|
|
196
|
+
} from './config-client-errors';
|
|
197
|
+
export type {
|
|
198
|
+
FailureCaptureConfig,
|
|
199
|
+
FailureCaptureSection,
|
|
200
|
+
FailureCaptureSectionInput,
|
|
201
|
+
} from './config-failure-capture';
|
|
202
|
+
export { FAILURE_CAPTURE_DEFAULTS } from './config-failure-capture';
|
|
146
203
|
export type { DrainConfig, HealthConfig, ReadinessMode } from './config-health';
|
|
147
204
|
export { READINESS_MODES } from './config-health';
|
|
148
205
|
export type { IslandsConfig, IslandsSection, IslandsSectionInput } from './config-islands';
|
|
@@ -210,12 +267,24 @@ export {
|
|
|
210
267
|
sectionPatterns,
|
|
211
268
|
sitemapSectionIssues,
|
|
212
269
|
} from './config-sitemap';
|
|
270
|
+
export type { AppTimeConfig, TimeSection, TimeSectionInput } from './config-time';
|
|
271
|
+
export { TIME_DEFAULTS } from './config-time';
|
|
213
272
|
export type { ConflictPolicy, ResolveConflictOptions, Row } from './conflict-policy';
|
|
214
273
|
export { resolveConflict } from './conflict-policy';
|
|
215
|
-
export type {
|
|
274
|
+
export type {
|
|
275
|
+
CompletionBound,
|
|
276
|
+
Ctx,
|
|
277
|
+
CtxFacts,
|
|
278
|
+
CtxInit,
|
|
279
|
+
CtxPatch,
|
|
280
|
+
CtxServices,
|
|
281
|
+
ServiceBag,
|
|
282
|
+
} from './context';
|
|
216
283
|
export {
|
|
284
|
+
abandonment,
|
|
217
285
|
ctxOf,
|
|
218
286
|
hasContext,
|
|
287
|
+
runToCompletion,
|
|
219
288
|
runWithContext,
|
|
220
289
|
throwIfAborted,
|
|
221
290
|
tryUseContext,
|
|
@@ -242,9 +311,30 @@ export {
|
|
|
242
311
|
export type { Page } from './cursor-page';
|
|
243
312
|
export { pageOf } from './cursor-page';
|
|
244
313
|
export { compareDecimalText } from './decimal-order';
|
|
314
|
+
// `errors:` on an action and a query: the rule both share, since the two packages are one tier.
|
|
315
|
+
export type { DeclaredErrorsOwner, DeclaredErrorVerdict, ErrorStatusOf } from './declared-errors';
|
|
316
|
+
export {
|
|
317
|
+
ActionErrorsInvalidError,
|
|
318
|
+
checkDeclaredErrors,
|
|
319
|
+
describeDeclaredErrors,
|
|
320
|
+
ErrorUndeclaredError,
|
|
321
|
+
enforceDeclared,
|
|
322
|
+
judgeThrown,
|
|
323
|
+
LIFETIME_ERROR_CODES,
|
|
324
|
+
withDeclaredErrors,
|
|
325
|
+
} from './declared-errors';
|
|
326
|
+
export type { DeclaredResponsesWire } from './declared-errors-openapi';
|
|
327
|
+
export { DECLARED_ERRORS_EXTENSION, withDeclaredResponses } from './declared-errors-openapi';
|
|
328
|
+
export type { SourceReader } from './declared-errors-scan';
|
|
329
|
+
export { definedErrorClassCodes, scanHandlerSource } from './declared-errors-scan';
|
|
245
330
|
export { DEFAULT_LOCALE, DEFAULT_TIME_ZONE } from './default-locale';
|
|
246
331
|
export type { Deprecation, DeprecationField, DeprecationRender } from './deprecation';
|
|
247
|
-
export {
|
|
332
|
+
export {
|
|
333
|
+
recordDeprecatedCall,
|
|
334
|
+
renderDeprecation,
|
|
335
|
+
successorLink,
|
|
336
|
+
successorPathOf,
|
|
337
|
+
} from './deprecation';
|
|
248
338
|
export type { DevSecretsOptions } from './dev-secrets';
|
|
249
339
|
export {
|
|
250
340
|
assertNoDevSecretsOutsideLocal,
|
|
@@ -255,6 +345,7 @@ export {
|
|
|
255
345
|
DRAIN_DEADLINE_DEFAULT_MS,
|
|
256
346
|
DRAIN_DEADLINE_MAX_MS,
|
|
257
347
|
WORKER_DRAIN_DEADLINE_MAX_MS,
|
|
348
|
+
WORKER_RETIRE_DEFAULT_MS,
|
|
258
349
|
} from './drain-deadline';
|
|
259
350
|
export type {
|
|
260
351
|
Env,
|
|
@@ -291,14 +382,37 @@ export {
|
|
|
291
382
|
resolveEnvironment,
|
|
292
383
|
tryResolveEnvironment,
|
|
293
384
|
} from './environment';
|
|
385
|
+
export { ExerciseEnvironmentError, ExerciseUnscopedError } from './exercise-errors';
|
|
386
|
+
// A contract exercise's one loop: `@ultimat3/action`'s and `@ultimat3/query`'s, one tier apart from neither.
|
|
387
|
+
export type {
|
|
388
|
+
DrawnInput,
|
|
389
|
+
ExerciseAttempt,
|
|
390
|
+
ExerciseReport,
|
|
391
|
+
ExerciseSubject,
|
|
392
|
+
ExerciseVerdict,
|
|
393
|
+
} from './exercise-run';
|
|
394
|
+
export {
|
|
395
|
+
assertExerciseScoped,
|
|
396
|
+
bindDrawn,
|
|
397
|
+
EXERCISE_RUNS,
|
|
398
|
+
EXERCISE_RUNS_MAX,
|
|
399
|
+
exerciseDefect,
|
|
400
|
+
exerciseReplay,
|
|
401
|
+
runExercise,
|
|
402
|
+
sealedHost,
|
|
403
|
+
} from './exercise-run';
|
|
294
404
|
export type {
|
|
295
405
|
CodedErrorInit,
|
|
296
406
|
CoreErrorCode,
|
|
407
|
+
DefinedError,
|
|
408
|
+
DefinedErrorExtra,
|
|
409
|
+
DefineErrorSpec,
|
|
297
410
|
ErrorAudience,
|
|
298
411
|
ErrorCodeDeclaration,
|
|
299
412
|
ErrorCodeDescriptor,
|
|
300
413
|
ErrorCodeEntry,
|
|
301
414
|
ErrorRetry,
|
|
415
|
+
ErrorText,
|
|
302
416
|
FormatErrorOptions,
|
|
303
417
|
UltimateErrorInit,
|
|
304
418
|
UltimateErrorJSON,
|
|
@@ -309,6 +423,7 @@ export {
|
|
|
309
423
|
classifyThrown,
|
|
310
424
|
DEFAULT_ERROR_RETRY,
|
|
311
425
|
declaredErrorRetry,
|
|
426
|
+
defineError,
|
|
312
427
|
deniedCallerFix,
|
|
313
428
|
describeErrorCode,
|
|
314
429
|
describeValue,
|
|
@@ -349,6 +464,7 @@ export {
|
|
|
349
464
|
} from './exports/error-contract';
|
|
350
465
|
export type {
|
|
351
466
|
AttributeValue,
|
|
467
|
+
CapturedValue,
|
|
352
468
|
Counter,
|
|
353
469
|
ErrorReport,
|
|
354
470
|
ErrorReporter,
|
|
@@ -356,8 +472,16 @@ export type {
|
|
|
356
472
|
ErrorScope,
|
|
357
473
|
ErrorSeverity,
|
|
358
474
|
ErrorSource,
|
|
475
|
+
FailureActor,
|
|
476
|
+
FailureBundle,
|
|
477
|
+
FailureCaptureLimits,
|
|
478
|
+
FailureFact,
|
|
479
|
+
FailurePrimitive,
|
|
480
|
+
FailureRead,
|
|
481
|
+
FailureStore,
|
|
359
482
|
Gauge,
|
|
360
483
|
GaugeOptions,
|
|
484
|
+
HeldFailure,
|
|
361
485
|
Histogram,
|
|
362
486
|
HistogramOptions,
|
|
363
487
|
HistogramPoint,
|
|
@@ -407,6 +531,8 @@ export type {
|
|
|
407
531
|
export {
|
|
408
532
|
alwaysOffSampler,
|
|
409
533
|
alwaysOnSampler,
|
|
534
|
+
captureValue,
|
|
535
|
+
chainErrorReporters,
|
|
410
536
|
collectMetrics,
|
|
411
537
|
configureErrorReporting,
|
|
412
538
|
configureMetrics,
|
|
@@ -422,10 +548,16 @@ export {
|
|
|
422
548
|
defaultSampler,
|
|
423
549
|
ERROR_SOURCES,
|
|
424
550
|
ErrorReporterDsnInvalidError,
|
|
551
|
+
errorFingerprint,
|
|
425
552
|
errorReport,
|
|
553
|
+
errorReportersOf,
|
|
426
554
|
exportMetrics,
|
|
555
|
+
failureCaptureActive,
|
|
556
|
+
failureCaptureLimits,
|
|
427
557
|
gauge,
|
|
428
558
|
histogram,
|
|
559
|
+
installedErrorReporter,
|
|
560
|
+
installedFailureStore,
|
|
429
561
|
isRedactedKey,
|
|
430
562
|
jobs,
|
|
431
563
|
LOG_LEVELS,
|
|
@@ -443,6 +575,7 @@ export {
|
|
|
443
575
|
noopErrorReporter,
|
|
444
576
|
noopExporter,
|
|
445
577
|
noopMetricExporter,
|
|
578
|
+
OTLP_RETRY_STATUSES,
|
|
446
579
|
OtlpEndpointInvalidError,
|
|
447
580
|
OtlpHeadersInvalidError,
|
|
448
581
|
OtlpProtocolUnsupportedError,
|
|
@@ -459,6 +592,8 @@ export {
|
|
|
459
592
|
REDACTED,
|
|
460
593
|
ratioSampler,
|
|
461
594
|
recordConnection,
|
|
595
|
+
recordFailureFact,
|
|
596
|
+
recordFailureInput,
|
|
462
597
|
recordJob,
|
|
463
598
|
recordLeaseLost,
|
|
464
599
|
recordQueueDepth,
|
|
@@ -468,6 +603,7 @@ export {
|
|
|
468
603
|
requestDuration,
|
|
469
604
|
requests,
|
|
470
605
|
resetErrorReporting,
|
|
606
|
+
resetFailureCaptureWindows,
|
|
471
607
|
resetMetrics,
|
|
472
608
|
resetTelemetry,
|
|
473
609
|
SCALING_METRICS,
|
|
@@ -475,13 +611,16 @@ export {
|
|
|
475
611
|
sentryEnvelope,
|
|
476
612
|
sentryErrorReporter,
|
|
477
613
|
serviceResource,
|
|
614
|
+
setFailureStore,
|
|
478
615
|
setLoggerContextFields,
|
|
479
616
|
setLogStream,
|
|
617
|
+
settleFailureCaptures,
|
|
480
618
|
startMetricExport,
|
|
481
619
|
startSpan,
|
|
482
620
|
structuredLogger,
|
|
483
621
|
traceparent,
|
|
484
622
|
tryOtlpEndpoint,
|
|
623
|
+
withFailureCapture,
|
|
485
624
|
withSpan,
|
|
486
625
|
withSpanContext,
|
|
487
626
|
} from './exports/observability';
|
|
@@ -537,7 +676,9 @@ export {
|
|
|
537
676
|
writeMasterKeyFile,
|
|
538
677
|
writeSecretsFile,
|
|
539
678
|
} from './exports/secrets';
|
|
540
|
-
export {
|
|
679
|
+
export type { FilledWire } from './fill-wire';
|
|
680
|
+
export { fillWire } from './fill-wire';
|
|
681
|
+
export { finiteCount, finiteOption, MAX_TIMER_MS } from './finite-option';
|
|
541
682
|
export type {
|
|
542
683
|
FlightGate,
|
|
543
684
|
FlightGateLimits,
|
|
@@ -551,6 +692,9 @@ export type { GenerationFence } from './generation-fence';
|
|
|
551
692
|
export { generationFence, isSuperseded } from './generation-fence';
|
|
552
693
|
export type { PublicHealthBody } from './health-disclosure';
|
|
553
694
|
export { DEFAULT_HEALTH_DETAIL_PEERS, healthBody, healthPeerListed } from './health-disclosure';
|
|
695
|
+
// The ONE keyed hash: every HMAC in the framework (`bun run keyed-hasher`).
|
|
696
|
+
export type { HmacAlgorithm, HmacEncoding, HmacInput } from './hmac';
|
|
697
|
+
export { hmac } from './hmac';
|
|
554
698
|
export type { HostDecision, HostRule } from './host-rules';
|
|
555
699
|
export { ANY_HOST, hostDecision, hostMatches } from './host-rules';
|
|
556
700
|
export { escapeHtml } from './html-escape';
|
|
@@ -617,6 +761,7 @@ export {
|
|
|
617
761
|
} from './intl-cache';
|
|
618
762
|
export { isIsoDateTime } from './iso-date';
|
|
619
763
|
export { isJsonObject } from './json-object';
|
|
764
|
+
export { jsonbRefusal } from './json-storable';
|
|
620
765
|
export type { FingerprintMatch } from './keyed-fingerprint';
|
|
621
766
|
export {
|
|
622
767
|
compareFingerprint,
|
|
@@ -712,6 +857,7 @@ export {
|
|
|
712
857
|
CLIENT_NAVIGATION_SURFACE_HEADER,
|
|
713
858
|
CLIENT_PATH_STYLE_META,
|
|
714
859
|
CLIENT_PERSIST_META,
|
|
860
|
+
CLIENT_PINS_META,
|
|
715
861
|
CLIENT_SCOPE_HEADER,
|
|
716
862
|
CLIENT_SCOPE_META,
|
|
717
863
|
CLIENT_SYNC_META,
|
|
@@ -756,11 +902,22 @@ export {
|
|
|
756
902
|
REQUEST_TIMEOUT_HEADER,
|
|
757
903
|
remainingBudgetMs,
|
|
758
904
|
} from './request-budget';
|
|
759
|
-
export type {
|
|
760
|
-
export { retry
|
|
905
|
+
export type { RetryDeps } from './retry';
|
|
906
|
+
export { retry } from './retry';
|
|
907
|
+
export type { RetryDecision, RetryPolicy, RetryStopReason } from './retry-loop';
|
|
908
|
+
export { retryDecision } from './retry-loop';
|
|
761
909
|
export { isRetryableStatus, RETRYABLE_STATUSES } from './retryable-status';
|
|
762
910
|
export type { ResolveRoleOptions, Role, RoleInfo, ScalingSignal } from './roles';
|
|
763
911
|
export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
|
|
912
|
+
export { HttpPathInputConflictError, mergePathParams } from './route-path-merge';
|
|
913
|
+
export type { FilledTemplate, TemplateKeyNode } from './route-path-template';
|
|
914
|
+
export {
|
|
915
|
+
fillTemplate,
|
|
916
|
+
pathTemplateProblem,
|
|
917
|
+
templateKeyProblem,
|
|
918
|
+
templateParams,
|
|
919
|
+
toOpenApiPath,
|
|
920
|
+
} from './route-path-template';
|
|
764
921
|
export { routeRank } from './route-rank';
|
|
765
922
|
export type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
|
|
766
923
|
export { HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, RENDER_MODES } from './route-vocabulary';
|
|
@@ -791,6 +948,7 @@ export {
|
|
|
791
948
|
resetServices,
|
|
792
949
|
type ServiceFactory,
|
|
793
950
|
} from './service';
|
|
951
|
+
export { onSessionLost, sessionIsLost } from './session-lost';
|
|
794
952
|
export type { FlightJoin, Scheduler, SingleFlight, SingleFlightOptions } from './single-flight';
|
|
795
953
|
export { singleFlight } from './single-flight';
|
|
796
954
|
export { endOfLiteral, maskLiterals, QUOTES, stripComments } from './source-mask';
|
|
@@ -798,12 +956,17 @@ export type { StoreMode } from './store-mode';
|
|
|
798
956
|
export { STORE_MODES, storeMode } from './store-mode';
|
|
799
957
|
export { THEME_STORAGE_KEY } from './theme-storage';
|
|
800
958
|
export { timingSafeEqual } from './timing-safe-equal';
|
|
959
|
+
// A projected tool's description: `@ultimat3/mcp`'s and `@ultimat3/ai`'s, one tier apart from neither.
|
|
960
|
+
export type { ToolDescriptionSource } from './tool-description';
|
|
961
|
+
export { toolDescription } from './tool-description';
|
|
801
962
|
export {
|
|
802
963
|
frameworkVersion,
|
|
803
964
|
readPackageVersion,
|
|
804
965
|
resolveVersion,
|
|
805
966
|
VERSION_DEFINE,
|
|
806
967
|
} from './version';
|
|
968
|
+
export type { VisibleIntervalOptions } from './visible-interval';
|
|
969
|
+
export { visibleInterval } from './visible-interval';
|
|
807
970
|
// The webhook wire format, at the tier both halves can reach — `@ultimat3/jobs` signs a delivery
|
|
808
971
|
// and `@ultimat3/http` verifies one, and neither may import the other. Same argument
|
|
809
972
|
// `timing-safe-equal.ts` makes for itself, one line above.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// What a `jsonb` column refuses of text `JSON.stringify` accepts. Asked of the text, by whoever is
|
|
2
|
+
// about to store it, so a memory store and Postgres answer alike and the refusal lands before a
|
|
3
|
+
// statement does: only production meets `jsonb`, after the work behind the value has already run.
|
|
4
|
+
|
|
5
|
+
/** One escape at a time, so the second backslash of `\\u0000` is never read as opening a third. */
|
|
6
|
+
const ESCAPE = /\\(?:u(?:0000|d[89a-f][0-9a-f]{2})|[\s\S])/g;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Why `jsonb` would refuse this `JSON.stringify` output, or `undefined` when it would not. A pair
|
|
10
|
+
* of surrogates is written raw by `JSON.stringify`, so an escaped surrogate in its output is always
|
|
11
|
+
* a lone one. The reason names the class of thing, never the text, which can carry anything.
|
|
12
|
+
*/
|
|
13
|
+
export function jsonbRefusal(text: string): string | undefined {
|
|
14
|
+
// Nearly every value holds no `\u` at all: the scan is paid only by one that does.
|
|
15
|
+
if (!text.includes('\\u')) return undefined;
|
|
16
|
+
for (const [token] of text.matchAll(ESCAPE)) {
|
|
17
|
+
if (token === '\\u0000') return 'a NUL character in a string, which jsonb has no form for';
|
|
18
|
+
if (token.startsWith('\\ud')) return 'a lone surrogate in a string, which jsonb refuses';
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
package/src/keyed-fingerprint.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { canonicalJson, fingerprint } from './canonical-json';
|
|
8
8
|
import { currentSigningSecret } from './cursor';
|
|
9
|
+
import { hmac } from './hmac';
|
|
9
10
|
import { timingSafeEqual } from './timing-safe-equal';
|
|
10
11
|
|
|
11
12
|
/** The format tag. A stored value without it predates keying (see `compareFingerprint`). */
|
|
@@ -23,9 +24,7 @@ const LEGACY = /^[0-9a-f]{16}$/;
|
|
|
23
24
|
|
|
24
25
|
/** One key per purpose, derived from the signing secret, so no two uses share a MAC key. */
|
|
25
26
|
function derivedKey(purpose: string): string {
|
|
26
|
-
return
|
|
27
|
-
.update(`ultimate:keyed-fingerprint:${purpose}`)
|
|
28
|
-
.digest('hex');
|
|
27
|
+
return hmac('sha256', currentSigningSecret(), `ultimate:keyed-fingerprint:${purpose}`, 'hex');
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
@@ -43,10 +42,7 @@ function keyId(key: string): string {
|
|
|
43
42
|
*/
|
|
44
43
|
export function keyedFingerprint(value: unknown, purpose: string): string {
|
|
45
44
|
const key = derivedKey(purpose);
|
|
46
|
-
const mac =
|
|
47
|
-
.update(canonicalJson(value))
|
|
48
|
-
.digest('hex')
|
|
49
|
-
.slice(0, 32);
|
|
45
|
+
const mac = hmac('sha256', key, canonicalJson(value), 'hex').slice(0, 32);
|
|
50
46
|
return `${KEYED_FINGERPRINT_VERSION}:${keyId(key)}:${mac}`;
|
|
51
47
|
}
|
|
52
48
|
|
package/src/lifecycle.ts
CHANGED
|
@@ -280,7 +280,11 @@ function remainingBudget(reason: ShutdownReason): number {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
async function runPhase(phase: ShutdownPhase, reason: ShutdownReason): Promise<void> {
|
|
283
|
-
|
|
283
|
+
const hooks = registrations.filter((entry) => entry.phase === phase);
|
|
284
|
+
// Resources are released in reverse of acquisition, and only `close` releases any: an exporter
|
|
285
|
+
// registered at boot flushes after what it serves has ended its last span.
|
|
286
|
+
if (phase === 'close') hooks.reverse();
|
|
287
|
+
for (const registration of hooks) {
|
|
284
288
|
const outcome = await settleWithin(() => registration.hook(reason), remainingBudget(reason));
|
|
285
289
|
if (outcome.kind === 'failed') {
|
|
286
290
|
report('error', 'shutdown hook failed', {
|
package/src/logger.ts
CHANGED
|
@@ -361,7 +361,7 @@ function timestamp(clock: Clock): string {
|
|
|
361
361
|
* `LOG_LEVEL`, and the default where there is no environment to read it from.
|
|
362
362
|
*
|
|
363
363
|
* A BROWSER has no `process` binding at all, and this read runs at MODULE INIT — `logger` at the
|
|
364
|
-
* foot of this file is `structuredLogger()` evaluated when the module is. Measured on
|
|
364
|
+
* foot of this file is `structuredLogger()` evaluated when the module is. Measured on one adopter's
|
|
365
365
|
* session console island: `@ultimat3/realtime`'s `channel.ts` calls `logger.warn`, so the shaker
|
|
366
366
|
* keeps `logger`, and the island's chunk died on `ReferenceError: process is not defined` before a
|
|
367
367
|
* line of the app's own code ran — the wrapper rendered `data-x-failed="process is not defined"`
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import {
|
|
14
14
|
assertFiniteOtlpBound,
|
|
15
15
|
OTLP_SCOPE,
|
|
16
|
+
type OtlpPostOptions,
|
|
16
17
|
otlpAttributes,
|
|
17
18
|
otlpEndpoint,
|
|
18
19
|
otlpHeaders,
|
|
@@ -100,6 +101,10 @@ export interface OtlpMetricExporterOptions {
|
|
|
100
101
|
readonly timeoutMs?: number | undefined;
|
|
101
102
|
/** Injected by tests; the preload seals the real one. */
|
|
102
103
|
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
104
|
+
/** The retry's wait, die and clock, passed to `postOtlp`. Injected by tests. */
|
|
105
|
+
readonly sleep?: OtlpPostOptions['sleep'];
|
|
106
|
+
readonly random?: OtlpPostOptions['random'];
|
|
107
|
+
readonly now?: OtlpPostOptions['now'];
|
|
103
108
|
/**
|
|
104
109
|
* Epoch ms this process's counters started at. Defaults to the first export, which is close
|
|
105
110
|
* enough for a rate and exact for a restart detector; pass the boot time when you have it.
|
|
@@ -121,6 +126,7 @@ export function otlpMetricExporter(options: OtlpMetricExporterOptions = {}): Otl
|
|
|
121
126
|
const headers = otlpHeaders(options.headers, process.env, 'metrics');
|
|
122
127
|
const timeoutMs = assertFiniteOtlpBound('timeoutMs', options.timeoutMs ?? 10_000);
|
|
123
128
|
const send = options.fetch ?? globalThis.fetch;
|
|
129
|
+
const seams = { sleep: options.sleep, random: options.random, now: options.now };
|
|
124
130
|
let startedAtMs = options.startedAtMs;
|
|
125
131
|
/** The ONE POST in flight, and the one body waiting behind it — never a chain. */
|
|
126
132
|
let sending: Promise<void> | undefined;
|
|
@@ -138,7 +144,7 @@ export function otlpMetricExporter(options: OtlpMetricExporterOptions = {}): Otl
|
|
|
138
144
|
const body = waiting;
|
|
139
145
|
waiting = undefined;
|
|
140
146
|
if (body === undefined) return Promise.resolve();
|
|
141
|
-
sending = postOtlp({ url, headers, body, timeoutMs, fetch: send })
|
|
147
|
+
sending = postOtlp({ url, headers, body, timeoutMs, fetch: send, ...seams })
|
|
142
148
|
.then(
|
|
143
149
|
() => undefined,
|
|
144
150
|
() => undefined,
|
package/src/otlp-post.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Single responsibility: deliver one OTLP/HTTP body, again when the collector says "not now". A
|
|
2
|
+
// rolling restart of a collector answers 503 to every pod at once, and a single-shot POST turned
|
|
3
|
+
// each of those answers into a lost window of traces on the surface an operator reads in an outage.
|
|
4
|
+
|
|
5
|
+
import type { Random } from './backoff';
|
|
6
|
+
import { retryAfterSecondsOf } from './client-retry-after';
|
|
7
|
+
import { systemClock } from './clock';
|
|
8
|
+
import { renderThrowable } from './error-render';
|
|
9
|
+
import { UltimateError } from './errors';
|
|
10
|
+
import { logger } from './logger';
|
|
11
|
+
import { type RetryPolicy, retryLoop } from './retry-loop';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The OTLP/HTTP specification's retryable answers, and only those. Not `isRetryableStatus`: that
|
|
15
|
+
* table answers "may a client repeat this call?", and a 500 or a 400 from a validating collector
|
|
16
|
+
* rejects the same batch identically on every attempt.
|
|
17
|
+
*/
|
|
18
|
+
export const OTLP_RETRY_STATUSES: ReadonlySet<number> = new Set([429, 502, 503, 504]);
|
|
19
|
+
|
|
20
|
+
/** Core's curve, the same shape `outbound()` defaults to; `timeoutMs` adds the time budget. */
|
|
21
|
+
const OTLP_RETRY_POLICY: Omit<RetryPolicy, 'timeBudgetMs'> = Object.freeze({
|
|
22
|
+
attempts: 3,
|
|
23
|
+
base: 250,
|
|
24
|
+
max: 5_000,
|
|
25
|
+
jitter: 'full',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export interface OtlpPostOptions {
|
|
29
|
+
readonly url: string;
|
|
30
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
31
|
+
readonly body: string;
|
|
32
|
+
/** The WHOLE export — every attempt and every wait between them — so a batch is held no longer. */
|
|
33
|
+
readonly timeoutMs: number;
|
|
34
|
+
readonly fetch: typeof globalThis.fetch;
|
|
35
|
+
/** Injected so a retry schedule is provable without waiting for one. Defaults to a timer. */
|
|
36
|
+
readonly sleep?: ((ms: number) => Promise<void>) | undefined;
|
|
37
|
+
/** The jitter's die. Defaults to `Math.random`. */
|
|
38
|
+
readonly random?: Random | undefined;
|
|
39
|
+
/** Monotonic ms the budget is measured on. Defaults to the system clock. */
|
|
40
|
+
readonly now?: (() => number) | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** How one export ended. Every one of them is a log line or silence, never a rejection. */
|
|
44
|
+
type Outcome =
|
|
45
|
+
| { readonly kind: 'delivered' }
|
|
46
|
+
| { readonly kind: 'rejected'; readonly status: number }
|
|
47
|
+
| { readonly kind: 'failed'; readonly error: unknown };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* why: an `UltimateError` only so `retryDecision` can classify it and read the stated delay off
|
|
51
|
+
* `meta.retryAfterSeconds` — `classifyThrown` reads nothing but that brand. It is control flow, the
|
|
52
|
+
* same rule a job's suspension follows: thrown inside `retryLoop`, caught in `postOtlp`, and never
|
|
53
|
+
* seen by a caller or a log line. `X_DRAINING` is the existing core code that means exactly this
|
|
54
|
+
* answer (the responder is restarting or shedding, try again), so no code is minted for a value
|
|
55
|
+
* that cannot escape.
|
|
56
|
+
*/
|
|
57
|
+
class OtlpRetryable extends UltimateError {
|
|
58
|
+
readonly outcome: Exclude<Outcome, { kind: 'delivered' }>;
|
|
59
|
+
constructor(outcome: Exclude<Outcome, { kind: 'delivered' }>, retryAfterSeconds?: number) {
|
|
60
|
+
super({
|
|
61
|
+
code: 'X_DRAINING',
|
|
62
|
+
cause: 'the OTLP collector asked for this batch again',
|
|
63
|
+
fix: 'postOtlp(request) # nothing to run: packages/core/src/otlp-post.ts sends the batch again itself',
|
|
64
|
+
retry: retryAfterSeconds === undefined ? 'retryable' : 'retry-after',
|
|
65
|
+
meta: retryAfterSeconds === undefined ? undefined : { retryAfterSeconds },
|
|
66
|
+
});
|
|
67
|
+
this.outcome = outcome;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A REF'D wait: it holds the process open, because `timeoutMs` already bounds it and the drain
|
|
73
|
+
* deadline bounds a shutdown. Unref'd, a shutdown flush answered 503 exited mid-wait — the batch
|
|
74
|
+
* lost and its line never printed. The same shape as `retry.ts`'s `abortableTimer`.
|
|
75
|
+
*/
|
|
76
|
+
const retryTimer = (ms: number): Promise<void> =>
|
|
77
|
+
new Promise<void>((done) => {
|
|
78
|
+
setTimeout(done, ms);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function logOutcome(url: string, outcome: Outcome, attempts: number): void {
|
|
82
|
+
if (outcome.kind === 'rejected') {
|
|
83
|
+
logger.warn('otlp export rejected', { url, status: outcome.status, attempts });
|
|
84
|
+
} else if (outcome.kind === 'failed') {
|
|
85
|
+
logger.warn('otlp export failed', {
|
|
86
|
+
url,
|
|
87
|
+
attempts,
|
|
88
|
+
// `renderThrowable`: the read that renders a caught value must not itself throw, or the
|
|
89
|
+
// export failure this function exists to swallow escapes as an unhandled rejection.
|
|
90
|
+
error: renderThrowable(outcome.error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Never throws and never rejects. Telemetry delivery is not the request: a collector that is down
|
|
97
|
+
* must not become the app's outage, and the alternative — an unhandled rejection from a `void`ed
|
|
98
|
+
* promise — takes the process with it on Bun. One line per batch, never per attempt, so a collector
|
|
99
|
+
* that stays down is not also a log flood.
|
|
100
|
+
*
|
|
101
|
+
* `retryLoop`, not `retry()`: `retry()` adopts the ambient request's signal and deadline, and a
|
|
102
|
+
* span batch flushed from inside a request would then die with that request. This send belongs to
|
|
103
|
+
* the process, and its one lifetime is `timeoutMs`.
|
|
104
|
+
*/
|
|
105
|
+
export async function postOtlp(options: OtlpPostOptions): Promise<void> {
|
|
106
|
+
const now = options.now ?? ((): number => systemClock.monotonic());
|
|
107
|
+
const startedAt = now();
|
|
108
|
+
let attempts = 0;
|
|
109
|
+
const send = async (): Promise<Outcome> => {
|
|
110
|
+
attempts += 1;
|
|
111
|
+
const remaining = Math.max(1, Math.ceil(options.timeoutMs - (now() - startedAt)));
|
|
112
|
+
const signal = AbortSignal.timeout(remaining);
|
|
113
|
+
let response: Response;
|
|
114
|
+
try {
|
|
115
|
+
response = await options.fetch(options.url, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { ...options.headers },
|
|
118
|
+
body: options.body,
|
|
119
|
+
signal,
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// The budget's own abort ends the export: what remained of `timeoutMs` is spent.
|
|
123
|
+
if (signal.aborted) return { kind: 'failed', error };
|
|
124
|
+
throw new OtlpRetryable({ kind: 'failed', error });
|
|
125
|
+
}
|
|
126
|
+
if (response.ok) return { kind: 'delivered' };
|
|
127
|
+
const rejected = { kind: 'rejected', status: response.status } as const;
|
|
128
|
+
if (!OTLP_RETRY_STATUSES.has(response.status)) return rejected;
|
|
129
|
+
const stated = retryAfterSecondsOf(
|
|
130
|
+
response.headers.get('retry-after'),
|
|
131
|
+
response.headers.get('date'),
|
|
132
|
+
);
|
|
133
|
+
throw new OtlpRetryable(rejected, stated);
|
|
134
|
+
};
|
|
135
|
+
let outcome: Outcome;
|
|
136
|
+
try {
|
|
137
|
+
outcome = await retryLoop(
|
|
138
|
+
send,
|
|
139
|
+
{ ...OTLP_RETRY_POLICY, timeBudgetMs: options.timeoutMs },
|
|
140
|
+
{ sleep: options.sleep ?? retryTimer, now, random: options.random },
|
|
141
|
+
);
|
|
142
|
+
} catch (failure) {
|
|
143
|
+
outcome =
|
|
144
|
+
failure instanceof OtlpRetryable ? failure.outcome : { kind: 'failed', error: failure };
|
|
145
|
+
}
|
|
146
|
+
logOutcome(options.url, outcome, attempts);
|
|
147
|
+
}
|