@ciq-dev/neoiq-foundation-node 1.0.1-beta.2 → 1.0.1-beta.4
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/dist/bootstrap.d.mts +1 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +31 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bootstrap.mjs +30 -0
- package/dist/bootstrap.mjs.map +1 -0
- package/dist/index.d.mts +95 -85
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +95 -85
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +165 -301
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +127 -245
- package/dist/index.mjs.map +1 -1
- package/dist/tracing-B8-Ntcll.js +312 -0
- package/dist/tracing-B8-Ntcll.js.map +1 -0
- package/dist/tracing-CcsyQIpB.mjs +192 -0
- package/dist/tracing-CcsyQIpB.mjs.map +1 -0
- package/package.json +12 -6
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
-
import { SpanStatusCode, SpanStatusCode as SpanStatusCode$1, context, context as context$1, metrics, propagation, propagation as propagation$1, trace, trace as trace$1 } from "@opentelemetry/api";
|
|
5
|
-
import pino from "pino";
|
|
6
|
-
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
7
|
-
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
8
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
1
|
+
import { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, context, getActiveSpan, getDefaultOtelEndpoint, getTraceContext, getTracer, isTracingEnabled, parseConfig, propagation, setupTracing, shutdownTracing, trace } from "./tracing-CcsyQIpB.mjs";
|
|
9
2
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
3
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
4
|
+
import { SpanStatusCode as SpanStatusCode$1, context as context$1, metrics, propagation as propagation$1, trace as trace$1 } from "@opentelemetry/api";
|
|
5
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
6
|
+
import pino from "pino";
|
|
11
7
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
12
8
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
13
9
|
import fp from "fastify-plugin";
|
|
@@ -15,90 +11,9 @@ import { randomUUID } from "crypto";
|
|
|
15
11
|
import axios from "axios";
|
|
16
12
|
import axiosRetry from "axios-retry";
|
|
17
13
|
import CircuitBreaker from "opossum";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
18
15
|
import { Readable } from "node:stream";
|
|
19
16
|
|
|
20
|
-
//#region rolldown:runtime
|
|
21
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/config.ts
|
|
25
|
-
const AutoInstrumentationConfigSchema = z.object({
|
|
26
|
-
http: z.boolean().default(true),
|
|
27
|
-
fastify: z.boolean().default(true),
|
|
28
|
-
express: z.boolean().default(true),
|
|
29
|
-
mongodb: z.boolean().default(true),
|
|
30
|
-
pg: z.boolean().default(true),
|
|
31
|
-
mysql: z.boolean().default(true),
|
|
32
|
-
redis: z.boolean().default(true),
|
|
33
|
-
ioredis: z.boolean().default(true),
|
|
34
|
-
grpc: z.boolean().default(true),
|
|
35
|
-
fs: z.boolean().default(false),
|
|
36
|
-
dns: z.boolean().default(false)
|
|
37
|
-
}).partial();
|
|
38
|
-
const FeaturesConfigSchema = z.object({
|
|
39
|
-
tracing: z.boolean().default(true),
|
|
40
|
-
metrics: z.boolean().default(true),
|
|
41
|
-
logging: z.boolean().default(true),
|
|
42
|
-
autoInstrumentation: AutoInstrumentationConfigSchema.default({})
|
|
43
|
-
}).partial();
|
|
44
|
-
const DEFAULT_OTEL_ENDPOINT = "http://otel-stack-deployment-collector.observability.svc.cluster.local:4317";
|
|
45
|
-
const OtelConfigSchema = z.object({
|
|
46
|
-
endpoint: z.string().default(DEFAULT_OTEL_ENDPOINT),
|
|
47
|
-
metricsIntervalMs: z.number().min(1e3).default(5e3),
|
|
48
|
-
traceSampleRate: z.number().min(0).max(1).default(1)
|
|
49
|
-
}).partial();
|
|
50
|
-
const LoggingConfigSchema = z.object({
|
|
51
|
-
level: z.enum([
|
|
52
|
-
"debug",
|
|
53
|
-
"info",
|
|
54
|
-
"warn",
|
|
55
|
-
"error"
|
|
56
|
-
]).default("info"),
|
|
57
|
-
prettyPrint: z.boolean().optional()
|
|
58
|
-
}).partial();
|
|
59
|
-
const RequestLoggingConfigSchema = z.object({
|
|
60
|
-
logHeaders: z.boolean().default(true),
|
|
61
|
-
logBody: z.boolean().default(false),
|
|
62
|
-
logResponseBody: z.boolean().default(false),
|
|
63
|
-
maxBodySize: z.number().default(10 * 1024),
|
|
64
|
-
redactHeaders: z.array(z.string()).optional()
|
|
65
|
-
}).partial();
|
|
66
|
-
const RedactionConfigSchema = z.object({ additionalPaths: z.array(z.string()).default([]) }).partial();
|
|
67
|
-
const ShutdownConfigSchema = z.object({
|
|
68
|
-
flushOnCrash: z.boolean().default(false),
|
|
69
|
-
flushTimeoutMs: z.number().min(100).default(5e3)
|
|
70
|
-
}).partial();
|
|
71
|
-
const FoundationConfigSchema = z.object({
|
|
72
|
-
serviceName: z.string().min(1, "serviceName is required"),
|
|
73
|
-
serviceVersion: z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
|
|
74
|
-
environment: z.enum([
|
|
75
|
-
"development",
|
|
76
|
-
"staging",
|
|
77
|
-
"qa",
|
|
78
|
-
"production"
|
|
79
|
-
]).default(process.env.NODE_ENV || "development"),
|
|
80
|
-
features: FeaturesConfigSchema.default({}),
|
|
81
|
-
otel: OtelConfigSchema.default({}),
|
|
82
|
-
logging: LoggingConfigSchema.default({}),
|
|
83
|
-
requestLogging: RequestLoggingConfigSchema.default({}),
|
|
84
|
-
redaction: RedactionConfigSchema.default({}),
|
|
85
|
-
shutdown: ShutdownConfigSchema.default({})
|
|
86
|
-
});
|
|
87
|
-
/** Parse and validate configuration */
|
|
88
|
-
function parseConfig(input) {
|
|
89
|
-
const result = FoundationConfigSchema.safeParse(input);
|
|
90
|
-
if (!result.success) {
|
|
91
|
-
const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
92
|
-
throw new Error(`Invalid foundation configuration:\n${errors}`);
|
|
93
|
-
}
|
|
94
|
-
return result.data;
|
|
95
|
-
}
|
|
96
|
-
/** Get default OTEL endpoint */
|
|
97
|
-
function getDefaultOtelEndpoint() {
|
|
98
|
-
return process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTEL_ENDPOINT;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
17
|
//#region src/features/context.ts
|
|
103
18
|
const BAGGAGE_CORRELATION_KEY = "correlation.id";
|
|
104
19
|
function setBaggageCorrelationId(correlationId) {
|
|
@@ -385,12 +300,13 @@ function wrapPinoLogger(pinoLogger) {
|
|
|
385
300
|
};
|
|
386
301
|
}
|
|
387
302
|
/** Fallback logger when Pino is not available */
|
|
388
|
-
function createFallbackLogger(serviceName = "unknown") {
|
|
303
|
+
function createFallbackLogger(serviceName = "unknown", baseBindings = {}) {
|
|
389
304
|
const log = (level, obj, msg) => {
|
|
390
305
|
const logObj = {
|
|
391
306
|
timestamp: new Date().toISOString(),
|
|
392
307
|
level,
|
|
393
308
|
service: serviceName,
|
|
309
|
+
...baseBindings,
|
|
394
310
|
...obj,
|
|
395
311
|
msg
|
|
396
312
|
};
|
|
@@ -404,7 +320,10 @@ function createFallbackLogger(serviceName = "unknown") {
|
|
|
404
320
|
info: (obj, msg) => log("info", obj, msg),
|
|
405
321
|
warn: (obj, msg) => log("warn", obj, msg),
|
|
406
322
|
error: (obj, msg) => log("error", obj, msg),
|
|
407
|
-
child: () =>
|
|
323
|
+
child: (bindings) => createFallbackLogger(serviceName, {
|
|
324
|
+
...baseBindings,
|
|
325
|
+
...bindings
|
|
326
|
+
}),
|
|
408
327
|
pino: null
|
|
409
328
|
};
|
|
410
329
|
return fallback;
|
|
@@ -417,98 +336,6 @@ function getGlobalLogger() {
|
|
|
417
336
|
return globalLogger || createFallbackLogger();
|
|
418
337
|
}
|
|
419
338
|
|
|
420
|
-
//#endregion
|
|
421
|
-
//#region src/features/tracing.ts
|
|
422
|
-
let sdk = null;
|
|
423
|
-
let isInitialized$1 = false;
|
|
424
|
-
const MONITORED_MODULES = [
|
|
425
|
-
"pg",
|
|
426
|
-
"mongodb",
|
|
427
|
-
"ioredis",
|
|
428
|
-
"mysql2",
|
|
429
|
-
"express",
|
|
430
|
-
"@grpc/grpc-js"
|
|
431
|
-
];
|
|
432
|
-
function warnPreloadedModules() {
|
|
433
|
-
for (const mod of MONITORED_MODULES) try {
|
|
434
|
-
if (__require.resolve(mod) in (__require.cache || {})) console.warn(`[neoiq-foundation] "${mod}" was imported before tracing init. Auto-instrumentation may not work for this module. Use node -r @ciq-dev/neoiq-foundation-node/bootstrap or call createFoundation() first.`);
|
|
435
|
-
} catch {}
|
|
436
|
-
}
|
|
437
|
-
/** Initialize OpenTelemetry tracing */
|
|
438
|
-
function setupTracing(options) {
|
|
439
|
-
if (isInitialized$1) {
|
|
440
|
-
console.warn("[neoiq-foundation] Tracing already initialized");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
warnPreloadedModules();
|
|
444
|
-
const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
|
|
445
|
-
const resource = resourceFromAttributes({
|
|
446
|
-
[ATTR_SERVICE_NAME]: serviceName,
|
|
447
|
-
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
448
|
-
"deployment.environment": environment
|
|
449
|
-
});
|
|
450
|
-
const traceExporter = new OTLPTraceExporter({ url: endpoint });
|
|
451
|
-
const instrumentationConfig = buildInstrumentationConfig(autoInstrumentation);
|
|
452
|
-
sdk = new NodeSDK({
|
|
453
|
-
resource,
|
|
454
|
-
traceExporter,
|
|
455
|
-
instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)]
|
|
456
|
-
});
|
|
457
|
-
sdk.start();
|
|
458
|
-
isInitialized$1 = true;
|
|
459
|
-
}
|
|
460
|
-
function buildInstrumentationConfig(config) {
|
|
461
|
-
const mapping = {
|
|
462
|
-
http: "@opentelemetry/instrumentation-http",
|
|
463
|
-
fastify: "@opentelemetry/instrumentation-fastify",
|
|
464
|
-
express: "@opentelemetry/instrumentation-express",
|
|
465
|
-
mongodb: "@opentelemetry/instrumentation-mongodb",
|
|
466
|
-
pg: "@opentelemetry/instrumentation-pg",
|
|
467
|
-
mysql: "@opentelemetry/instrumentation-mysql",
|
|
468
|
-
redis: "@opentelemetry/instrumentation-redis",
|
|
469
|
-
ioredis: "@opentelemetry/instrumentation-ioredis",
|
|
470
|
-
grpc: "@opentelemetry/instrumentation-grpc",
|
|
471
|
-
fs: "@opentelemetry/instrumentation-fs",
|
|
472
|
-
dns: "@opentelemetry/instrumentation-dns"
|
|
473
|
-
};
|
|
474
|
-
const result = {};
|
|
475
|
-
for (const [key, instrumentationName] of Object.entries(mapping)) {
|
|
476
|
-
const userSetting = config[key];
|
|
477
|
-
const defaultValue = key !== "fs" && key !== "dns";
|
|
478
|
-
result[instrumentationName] = { enabled: userSetting ?? defaultValue };
|
|
479
|
-
}
|
|
480
|
-
return result;
|
|
481
|
-
}
|
|
482
|
-
/** Shutdown tracing gracefully */
|
|
483
|
-
async function shutdownTracing() {
|
|
484
|
-
if (!sdk) return;
|
|
485
|
-
try {
|
|
486
|
-
await sdk.shutdown();
|
|
487
|
-
isInitialized$1 = false;
|
|
488
|
-
sdk = null;
|
|
489
|
-
} catch (error) {
|
|
490
|
-
console.error("[neoiq-foundation] Error shutting down tracing:", error);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function getTracer(name) {
|
|
494
|
-
return trace.getTracer(name);
|
|
495
|
-
}
|
|
496
|
-
function getActiveSpan() {
|
|
497
|
-
return trace.getActiveSpan();
|
|
498
|
-
}
|
|
499
|
-
function getTraceContext() {
|
|
500
|
-
const span = trace.getActiveSpan();
|
|
501
|
-
if (!span) return {};
|
|
502
|
-
const ctx = span.spanContext();
|
|
503
|
-
return {
|
|
504
|
-
traceId: ctx.traceId,
|
|
505
|
-
spanId: ctx.spanId
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
function isTracingEnabled() {
|
|
509
|
-
return isInitialized$1;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
339
|
//#endregion
|
|
513
340
|
//#region src/features/metrics.ts
|
|
514
341
|
let meterProvider = null;
|
|
@@ -546,6 +373,7 @@ async function shutdownMetrics() {
|
|
|
546
373
|
meterProvider = null;
|
|
547
374
|
} catch (error) {
|
|
548
375
|
console.error("[neoiq-foundation] Error shutting down metrics:", error);
|
|
376
|
+
throw error;
|
|
549
377
|
}
|
|
550
378
|
}
|
|
551
379
|
function getMeter(name, version = "1.0.0") {
|
|
@@ -823,31 +651,43 @@ function createHttpClient(options) {
|
|
|
823
651
|
}
|
|
824
652
|
return Promise.reject(error);
|
|
825
653
|
});
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
654
|
+
if (retry.enabled !== false) {
|
|
655
|
+
const retryConfig = {
|
|
656
|
+
retries: retry.retries ?? 3,
|
|
657
|
+
retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
|
|
658
|
+
retryCondition: (error) => {
|
|
659
|
+
const method = (error.config?.method ?? "get").toUpperCase();
|
|
660
|
+
const IDEMPOTENT = new Set([
|
|
661
|
+
"GET",
|
|
662
|
+
"HEAD",
|
|
663
|
+
"OPTIONS",
|
|
664
|
+
"PUT",
|
|
665
|
+
"DELETE"
|
|
666
|
+
]);
|
|
667
|
+
if (!retry.retryNonIdempotent && !IDEMPOTENT.has(method)) return false;
|
|
668
|
+
const retryStatusCodes = retry.retryStatusCodes ?? [
|
|
669
|
+
408,
|
|
670
|
+
429,
|
|
671
|
+
500,
|
|
672
|
+
502,
|
|
673
|
+
503,
|
|
674
|
+
504
|
|
675
|
+
];
|
|
676
|
+
return !error.response || retryStatusCodes.includes(error.response?.status || 0);
|
|
677
|
+
},
|
|
678
|
+
onRetry: (retryCount, error, requestConfig) => {
|
|
679
|
+
logger.warn({
|
|
680
|
+
retryCount,
|
|
681
|
+
url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
|
|
682
|
+
error: error.message
|
|
683
|
+
}, "Retrying request");
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
axiosRetry(client, retryConfig);
|
|
687
|
+
}
|
|
688
|
+
if (cbOptions.enabled === true) {
|
|
689
|
+
const originalRequest = client.request.bind(client);
|
|
690
|
+
const breaker = new CircuitBreaker(async (config) => originalRequest(config), {
|
|
851
691
|
timeout,
|
|
852
692
|
resetTimeout: cbOptions.resetTimeout ?? 3e4,
|
|
853
693
|
errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
|
|
@@ -856,7 +696,6 @@ function createHttpClient(options) {
|
|
|
856
696
|
breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
|
|
857
697
|
breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
|
|
858
698
|
breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
|
|
859
|
-
const originalRequest = client.request.bind(client);
|
|
860
699
|
client.request = (config) => breaker.fire(config);
|
|
861
700
|
client.get = (url, config) => breaker.fire({
|
|
862
701
|
...config,
|
|
@@ -894,10 +733,15 @@ function createHttpClient(options) {
|
|
|
894
733
|
//#endregion
|
|
895
734
|
//#region src/foundation.ts
|
|
896
735
|
const deprecationWarnings = new Set();
|
|
897
|
-
function warnDeprecation(oldPath, newPath) {
|
|
736
|
+
function warnDeprecation(oldPath, newPath, logger) {
|
|
898
737
|
if (deprecationWarnings.has(oldPath)) return;
|
|
899
738
|
deprecationWarnings.add(oldPath);
|
|
900
|
-
|
|
739
|
+
const msg = `foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`;
|
|
740
|
+
if (logger) logger.warn({
|
|
741
|
+
deprecated: oldPath,
|
|
742
|
+
replacement: newPath
|
|
743
|
+
}, msg);
|
|
744
|
+
else console.warn(`[neoiq-foundation] DEPRECATED: ${msg}`);
|
|
901
745
|
}
|
|
902
746
|
/** Create a fully configured observability foundation */
|
|
903
747
|
function createFoundation(input) {
|
|
@@ -955,7 +799,6 @@ function createFoundation(input) {
|
|
|
955
799
|
serviceVersion,
|
|
956
800
|
environment,
|
|
957
801
|
endpoint: otel.endpoint,
|
|
958
|
-
sampleRate: otel.traceSampleRate,
|
|
959
802
|
autoInstrumentation: featuresConfig.autoInstrumentation
|
|
960
803
|
});
|
|
961
804
|
tracerInstance = getTracer(serviceName);
|
|
@@ -1058,9 +901,11 @@ function createFoundation(input) {
|
|
|
1058
901
|
const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
|
|
1059
902
|
const loggingUp = !loggingError;
|
|
1060
903
|
const allUp = tracingUp && metricsUp && loggingUp;
|
|
1061
|
-
const
|
|
904
|
+
const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && !loggingUp;
|
|
905
|
+
let status = "healthy";
|
|
906
|
+
if (!allUp) status = allDown ? "unhealthy" : "degraded";
|
|
1062
907
|
return {
|
|
1063
|
-
status
|
|
908
|
+
status,
|
|
1064
909
|
timestamp: new Date().toISOString(),
|
|
1065
910
|
service: serviceName,
|
|
1066
911
|
version: serviceVersion,
|
|
@@ -1134,43 +979,43 @@ function createFoundation(input) {
|
|
|
1134
979
|
tracer: tracerInstance,
|
|
1135
980
|
meter: meterInstance,
|
|
1136
981
|
getTracer: (name) => {
|
|
1137
|
-
warnDeprecation("getTracer", "observability.getTracer");
|
|
982
|
+
warnDeprecation("getTracer", "observability.getTracer", logger);
|
|
1138
983
|
return observability.getTracer(name);
|
|
1139
984
|
},
|
|
1140
985
|
getMeter: (name, version) => {
|
|
1141
|
-
warnDeprecation("getMeter", "observability.getMeter");
|
|
986
|
+
warnDeprecation("getMeter", "observability.getMeter", logger);
|
|
1142
987
|
return observability.getMeter(name, version);
|
|
1143
988
|
},
|
|
1144
989
|
getTraceContext: () => {
|
|
1145
|
-
warnDeprecation("getTraceContext", "observability.getTraceContext");
|
|
990
|
+
warnDeprecation("getTraceContext", "observability.getTraceContext", logger);
|
|
1146
991
|
return observability.getTraceContext();
|
|
1147
992
|
},
|
|
1148
993
|
getActiveSpan: () => {
|
|
1149
|
-
warnDeprecation("getActiveSpan", "observability.getActiveSpan");
|
|
994
|
+
warnDeprecation("getActiveSpan", "observability.getActiveSpan", logger);
|
|
1150
995
|
return observability.getActiveSpan();
|
|
1151
996
|
},
|
|
1152
997
|
createHttpClient: (options) => {
|
|
1153
|
-
warnDeprecation("createHttpClient", "http.createClient");
|
|
998
|
+
warnDeprecation("createHttpClient", "http.createClient", logger);
|
|
1154
999
|
return httpModule.createClient(options);
|
|
1155
1000
|
},
|
|
1156
1001
|
shutdown: () => {
|
|
1157
|
-
warnDeprecation("shutdown", "lifecycle.shutdown");
|
|
1002
|
+
warnDeprecation("shutdown", "lifecycle.shutdown", logger);
|
|
1158
1003
|
return lifecycle.shutdown();
|
|
1159
1004
|
},
|
|
1160
1005
|
isReady: () => {
|
|
1161
|
-
warnDeprecation("isReady", "lifecycle.isReady");
|
|
1006
|
+
warnDeprecation("isReady", "lifecycle.isReady", logger);
|
|
1162
1007
|
return lifecycle.isReady();
|
|
1163
1008
|
},
|
|
1164
1009
|
health: () => {
|
|
1165
|
-
warnDeprecation("health", "lifecycle.health");
|
|
1010
|
+
warnDeprecation("health", "lifecycle.health", logger);
|
|
1166
1011
|
return lifecycle.health();
|
|
1167
1012
|
},
|
|
1168
1013
|
trace: (name, fn) => {
|
|
1169
|
-
warnDeprecation("trace", "observability.trace");
|
|
1014
|
+
warnDeprecation("trace", "observability.trace", logger);
|
|
1170
1015
|
return observability.trace(name, fn);
|
|
1171
1016
|
},
|
|
1172
1017
|
safeRun: (fn, fallback) => {
|
|
1173
|
-
warnDeprecation("safeRun", "lifecycle.safeRun");
|
|
1018
|
+
warnDeprecation("safeRun", "lifecycle.safeRun", logger);
|
|
1174
1019
|
return lifecycle.safeRun(fn, fallback);
|
|
1175
1020
|
}
|
|
1176
1021
|
};
|
|
@@ -1181,6 +1026,9 @@ const setupObservability = createFoundation;
|
|
|
1181
1026
|
|
|
1182
1027
|
//#endregion
|
|
1183
1028
|
//#region src/integrations/object-store/aws-s3.ts
|
|
1029
|
+
function isAwsError(err) {
|
|
1030
|
+
return typeof err === "object" && err !== null;
|
|
1031
|
+
}
|
|
1184
1032
|
async function loadAwsS3() {
|
|
1185
1033
|
try {
|
|
1186
1034
|
const clientMod = await import("@aws-sdk/client-s3");
|
|
@@ -1195,13 +1043,10 @@ async function loadAwsS3() {
|
|
|
1195
1043
|
getSignedUrl: presignerMod.getSignedUrl
|
|
1196
1044
|
};
|
|
1197
1045
|
} catch (err) {
|
|
1198
|
-
const e = err;
|
|
1046
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1199
1047
|
throw new Error(`AWS SDK not available. Install optional peer deps: @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner. Original error: ${e.message}`);
|
|
1200
1048
|
}
|
|
1201
1049
|
}
|
|
1202
|
-
function normalizeBody(body) {
|
|
1203
|
-
return body;
|
|
1204
|
-
}
|
|
1205
1050
|
/**
|
|
1206
1051
|
* AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
|
|
1207
1052
|
*
|
|
@@ -1230,7 +1075,7 @@ var AwsS3ObjectStore = class {
|
|
|
1230
1075
|
const res = await s3.send(new aws.PutObjectCommand({
|
|
1231
1076
|
Bucket: ref.bucket,
|
|
1232
1077
|
Key: ref.key,
|
|
1233
|
-
Body:
|
|
1078
|
+
Body: body,
|
|
1234
1079
|
ContentType: options.contentType,
|
|
1235
1080
|
CacheControl: options.cacheControl,
|
|
1236
1081
|
Metadata: options.metadata
|
|
@@ -1280,10 +1125,12 @@ var AwsS3ObjectStore = class {
|
|
|
1280
1125
|
lastModified: res.LastModified
|
|
1281
1126
|
};
|
|
1282
1127
|
} catch (err) {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1128
|
+
if (isAwsError(err)) {
|
|
1129
|
+
const name = String(err.name ?? "");
|
|
1130
|
+
if (name === "NoSuchBucket") throw err;
|
|
1131
|
+
const httpStatus = err.$metadata?.httpStatusCode;
|
|
1132
|
+
if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
|
|
1133
|
+
}
|
|
1287
1134
|
throw err;
|
|
1288
1135
|
}
|
|
1289
1136
|
}
|
|
@@ -1304,8 +1151,9 @@ var AwsS3ObjectStore = class {
|
|
|
1304
1151
|
ContinuationToken: options.continuationToken,
|
|
1305
1152
|
MaxKeys: options.maxKeys
|
|
1306
1153
|
}));
|
|
1154
|
+
const contents = res?.Contents ?? [];
|
|
1307
1155
|
return {
|
|
1308
|
-
objects: (
|
|
1156
|
+
objects: contents.filter((o) => typeof o.Key === "string").map((o) => ({
|
|
1309
1157
|
key: o.Key,
|
|
1310
1158
|
size: o.Size,
|
|
1311
1159
|
etag: o.ETag,
|
|
@@ -1336,19 +1184,31 @@ var AwsS3ObjectStore = class {
|
|
|
1336
1184
|
|
|
1337
1185
|
//#endregion
|
|
1338
1186
|
//#region src/integrations/object-store/in-memory.ts
|
|
1187
|
+
const DEFAULT_MAX_OBJECTS = 1e4;
|
|
1188
|
+
const STREAM_TIMEOUT_MS = 3e4;
|
|
1339
1189
|
function toBuffer(body) {
|
|
1340
1190
|
if (typeof body === "string") return Buffer.from(body);
|
|
1341
1191
|
if (Buffer.isBuffer(body)) return body;
|
|
1342
1192
|
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
1343
1193
|
return new Promise((resolve, reject) => {
|
|
1344
1194
|
const chunks = [];
|
|
1195
|
+
const timer = setTimeout(() => {
|
|
1196
|
+
body.destroy(new Error("InMemoryObjectStore: stream read timed out"));
|
|
1197
|
+
reject(new Error("InMemoryObjectStore: stream read timed out"));
|
|
1198
|
+
}, STREAM_TIMEOUT_MS);
|
|
1345
1199
|
body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
1346
|
-
body.on("end", () =>
|
|
1347
|
-
|
|
1200
|
+
body.on("end", () => {
|
|
1201
|
+
clearTimeout(timer);
|
|
1202
|
+
resolve(Buffer.concat(chunks));
|
|
1203
|
+
});
|
|
1204
|
+
body.on("error", (err) => {
|
|
1205
|
+
clearTimeout(timer);
|
|
1206
|
+
reject(err);
|
|
1207
|
+
});
|
|
1348
1208
|
});
|
|
1349
1209
|
}
|
|
1350
|
-
function
|
|
1351
|
-
return `
|
|
1210
|
+
function computeEtag(buf) {
|
|
1211
|
+
return `"${createHash("md5").update(buf).digest("hex")}"`;
|
|
1352
1212
|
}
|
|
1353
1213
|
/**
|
|
1354
1214
|
* InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
|
|
@@ -1357,6 +1217,11 @@ function simpleEtag(buf) {
|
|
|
1357
1217
|
var InMemoryObjectStore = class {
|
|
1358
1218
|
provider = "in-memory";
|
|
1359
1219
|
buckets = new Map();
|
|
1220
|
+
maxObjects;
|
|
1221
|
+
objectCount = 0;
|
|
1222
|
+
constructor(options = {}) {
|
|
1223
|
+
this.maxObjects = options.maxObjects ?? DEFAULT_MAX_OBJECTS;
|
|
1224
|
+
}
|
|
1360
1225
|
bucketMap(bucket) {
|
|
1361
1226
|
let m = this.buckets.get(bucket);
|
|
1362
1227
|
if (!m) {
|
|
@@ -1367,15 +1232,19 @@ var InMemoryObjectStore = class {
|
|
|
1367
1232
|
}
|
|
1368
1233
|
async putObject(ref, body, options = {}) {
|
|
1369
1234
|
const buf = await toBuffer(body);
|
|
1235
|
+
const map = this.bucketMap(ref.bucket);
|
|
1236
|
+
const isNew = !map.has(ref.key);
|
|
1237
|
+
if (isNew && this.objectCount >= this.maxObjects) throw new Error(`InMemoryObjectStore: max object limit reached (${this.maxObjects})`);
|
|
1370
1238
|
const obj = {
|
|
1371
1239
|
body: buf,
|
|
1372
1240
|
contentType: options.contentType,
|
|
1373
1241
|
cacheControl: options.cacheControl,
|
|
1374
1242
|
metadata: options.metadata,
|
|
1375
|
-
etag:
|
|
1243
|
+
etag: computeEtag(buf),
|
|
1376
1244
|
lastModified: new Date()
|
|
1377
1245
|
};
|
|
1378
|
-
|
|
1246
|
+
map.set(ref.key, obj);
|
|
1247
|
+
if (isNew) this.objectCount++;
|
|
1379
1248
|
return { etag: obj.etag };
|
|
1380
1249
|
}
|
|
1381
1250
|
async getObject(ref) {
|
|
@@ -1407,18 +1276,31 @@ var InMemoryObjectStore = class {
|
|
|
1407
1276
|
};
|
|
1408
1277
|
}
|
|
1409
1278
|
async deleteObject(ref) {
|
|
1410
|
-
this.bucketMap(ref.bucket)
|
|
1279
|
+
const map = this.bucketMap(ref.bucket);
|
|
1280
|
+
if (map.delete(ref.key)) this.objectCount--;
|
|
1411
1281
|
}
|
|
1412
1282
|
async listObjects(options) {
|
|
1413
|
-
const { bucket, prefix = "" } = options;
|
|
1283
|
+
const { bucket, prefix = "", continuationToken } = options;
|
|
1284
|
+
const maxKeys = Math.max(1, Math.floor(options.maxKeys ?? 1e3));
|
|
1414
1285
|
const map = this.bucketMap(bucket);
|
|
1415
|
-
const
|
|
1286
|
+
const all = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
|
|
1416
1287
|
key,
|
|
1417
1288
|
size: obj.body.length,
|
|
1418
1289
|
etag: obj.etag,
|
|
1419
1290
|
lastModified: obj.lastModified
|
|
1420
1291
|
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
1421
|
-
|
|
1292
|
+
let startIndex = 0;
|
|
1293
|
+
if (continuationToken) {
|
|
1294
|
+
const idx = all.findIndex((o) => o.key === continuationToken);
|
|
1295
|
+
if (idx === -1) throw new Error(`InMemoryObjectStore: invalid continuationToken "${continuationToken}"`);
|
|
1296
|
+
startIndex = idx + 1;
|
|
1297
|
+
}
|
|
1298
|
+
const page = all.slice(startIndex, startIndex + maxKeys);
|
|
1299
|
+
const hasMore = startIndex + maxKeys < all.length;
|
|
1300
|
+
return {
|
|
1301
|
+
objects: page,
|
|
1302
|
+
nextContinuationToken: hasMore ? page[page.length - 1]?.key : void 0
|
|
1303
|
+
};
|
|
1422
1304
|
}
|
|
1423
1305
|
async presignGetObject(_ref, _options) {
|
|
1424
1306
|
throw new Error("InMemoryObjectStore does not support presigned URLs");
|