@ciq-dev/neoiq-foundation-node 1.0.1-beta.2 → 1.0.1-beta.3

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/index.mjs CHANGED
@@ -1,13 +1,9 @@
1
- import { createRequire } from "module";
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-Cv-Y3fZx.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: () => fallback,
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,34 @@ function createHttpClient(options) {
823
651
  }
824
652
  return Promise.reject(error);
825
653
  });
826
- const retryConfig = {
827
- retries: retry.retries ?? 3,
828
- retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
829
- retryCondition: (error) => {
830
- const retryStatusCodes = retry.retryStatusCodes ?? [
831
- 408,
832
- 429,
833
- 500,
834
- 502,
835
- 503,
836
- 504
837
- ];
838
- return !error.response || retryStatusCodes.includes(error.response?.status || 0);
839
- },
840
- onRetry: (retryCount, error, requestConfig) => {
841
- logger.warn({
842
- retryCount,
843
- url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
844
- error: error.message
845
- }, "Retrying request");
846
- }
847
- };
848
- axiosRetry(client, retryConfig);
849
- if (cbOptions.enabled !== false) {
850
- const breaker = new CircuitBreaker(async (config) => client.request(config), {
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 retryStatusCodes = retry.retryStatusCodes ?? [
660
+ 408,
661
+ 429,
662
+ 500,
663
+ 502,
664
+ 503,
665
+ 504
666
+ ];
667
+ return !error.response || retryStatusCodes.includes(error.response?.status || 0);
668
+ },
669
+ onRetry: (retryCount, error, requestConfig) => {
670
+ logger.warn({
671
+ retryCount,
672
+ url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
673
+ error: error.message
674
+ }, "Retrying request");
675
+ }
676
+ };
677
+ axiosRetry(client, retryConfig);
678
+ }
679
+ if (cbOptions.enabled === true) {
680
+ const originalRequest = client.request.bind(client);
681
+ const breaker = new CircuitBreaker(async (config) => originalRequest(config), {
851
682
  timeout,
852
683
  resetTimeout: cbOptions.resetTimeout ?? 3e4,
853
684
  errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
@@ -856,7 +687,6 @@ function createHttpClient(options) {
856
687
  breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
857
688
  breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
858
689
  breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
859
- const originalRequest = client.request.bind(client);
860
690
  client.request = (config) => breaker.fire(config);
861
691
  client.get = (url, config) => breaker.fire({
862
692
  ...config,
@@ -894,10 +724,15 @@ function createHttpClient(options) {
894
724
  //#endregion
895
725
  //#region src/foundation.ts
896
726
  const deprecationWarnings = new Set();
897
- function warnDeprecation(oldPath, newPath) {
727
+ function warnDeprecation(oldPath, newPath, logger) {
898
728
  if (deprecationWarnings.has(oldPath)) return;
899
729
  deprecationWarnings.add(oldPath);
900
- console.warn(`[neoiq-foundation] DEPRECATED: foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`);
730
+ const msg = `foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`;
731
+ if (logger) logger.warn({
732
+ deprecated: oldPath,
733
+ replacement: newPath
734
+ }, msg);
735
+ else console.warn(`[neoiq-foundation] DEPRECATED: ${msg}`);
901
736
  }
902
737
  /** Create a fully configured observability foundation */
903
738
  function createFoundation(input) {
@@ -1058,9 +893,11 @@ function createFoundation(input) {
1058
893
  const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
1059
894
  const loggingUp = !loggingError;
1060
895
  const allUp = tracingUp && metricsUp && loggingUp;
1061
- const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
896
+ const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && !loggingUp;
897
+ let status = "healthy";
898
+ if (!allUp) status = allDown ? "unhealthy" : "degraded";
1062
899
  return {
1063
- status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
900
+ status,
1064
901
  timestamp: new Date().toISOString(),
1065
902
  service: serviceName,
1066
903
  version: serviceVersion,
@@ -1134,43 +971,43 @@ function createFoundation(input) {
1134
971
  tracer: tracerInstance,
1135
972
  meter: meterInstance,
1136
973
  getTracer: (name) => {
1137
- warnDeprecation("getTracer", "observability.getTracer");
974
+ warnDeprecation("getTracer", "observability.getTracer", logger);
1138
975
  return observability.getTracer(name);
1139
976
  },
1140
977
  getMeter: (name, version) => {
1141
- warnDeprecation("getMeter", "observability.getMeter");
978
+ warnDeprecation("getMeter", "observability.getMeter", logger);
1142
979
  return observability.getMeter(name, version);
1143
980
  },
1144
981
  getTraceContext: () => {
1145
- warnDeprecation("getTraceContext", "observability.getTraceContext");
982
+ warnDeprecation("getTraceContext", "observability.getTraceContext", logger);
1146
983
  return observability.getTraceContext();
1147
984
  },
1148
985
  getActiveSpan: () => {
1149
- warnDeprecation("getActiveSpan", "observability.getActiveSpan");
986
+ warnDeprecation("getActiveSpan", "observability.getActiveSpan", logger);
1150
987
  return observability.getActiveSpan();
1151
988
  },
1152
989
  createHttpClient: (options) => {
1153
- warnDeprecation("createHttpClient", "http.createClient");
990
+ warnDeprecation("createHttpClient", "http.createClient", logger);
1154
991
  return httpModule.createClient(options);
1155
992
  },
1156
993
  shutdown: () => {
1157
- warnDeprecation("shutdown", "lifecycle.shutdown");
994
+ warnDeprecation("shutdown", "lifecycle.shutdown", logger);
1158
995
  return lifecycle.shutdown();
1159
996
  },
1160
997
  isReady: () => {
1161
- warnDeprecation("isReady", "lifecycle.isReady");
998
+ warnDeprecation("isReady", "lifecycle.isReady", logger);
1162
999
  return lifecycle.isReady();
1163
1000
  },
1164
1001
  health: () => {
1165
- warnDeprecation("health", "lifecycle.health");
1002
+ warnDeprecation("health", "lifecycle.health", logger);
1166
1003
  return lifecycle.health();
1167
1004
  },
1168
1005
  trace: (name, fn) => {
1169
- warnDeprecation("trace", "observability.trace");
1006
+ warnDeprecation("trace", "observability.trace", logger);
1170
1007
  return observability.trace(name, fn);
1171
1008
  },
1172
1009
  safeRun: (fn, fallback) => {
1173
- warnDeprecation("safeRun", "lifecycle.safeRun");
1010
+ warnDeprecation("safeRun", "lifecycle.safeRun", logger);
1174
1011
  return lifecycle.safeRun(fn, fallback);
1175
1012
  }
1176
1013
  };
@@ -1181,6 +1018,9 @@ const setupObservability = createFoundation;
1181
1018
 
1182
1019
  //#endregion
1183
1020
  //#region src/integrations/object-store/aws-s3.ts
1021
+ function isAwsError(err) {
1022
+ return typeof err === "object" && err !== null;
1023
+ }
1184
1024
  async function loadAwsS3() {
1185
1025
  try {
1186
1026
  const clientMod = await import("@aws-sdk/client-s3");
@@ -1195,13 +1035,10 @@ async function loadAwsS3() {
1195
1035
  getSignedUrl: presignerMod.getSignedUrl
1196
1036
  };
1197
1037
  } catch (err) {
1198
- const e = err;
1038
+ const e = err instanceof Error ? err : new Error(String(err));
1199
1039
  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
1040
  }
1201
1041
  }
1202
- function normalizeBody(body) {
1203
- return body;
1204
- }
1205
1042
  /**
1206
1043
  * AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
1207
1044
  *
@@ -1230,7 +1067,7 @@ var AwsS3ObjectStore = class {
1230
1067
  const res = await s3.send(new aws.PutObjectCommand({
1231
1068
  Bucket: ref.bucket,
1232
1069
  Key: ref.key,
1233
- Body: normalizeBody(body),
1070
+ Body: body,
1234
1071
  ContentType: options.contentType,
1235
1072
  CacheControl: options.cacheControl,
1236
1073
  Metadata: options.metadata
@@ -1280,10 +1117,11 @@ var AwsS3ObjectStore = class {
1280
1117
  lastModified: res.LastModified
1281
1118
  };
1282
1119
  } catch (err) {
1283
- const e = err;
1284
- const name = String(e?.name || "");
1285
- const httpStatus = e?.$metadata?.httpStatusCode;
1286
- if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
1120
+ if (isAwsError(err)) {
1121
+ const name = String(err.name ?? "");
1122
+ const httpStatus = err.$metadata?.httpStatusCode;
1123
+ if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
1124
+ }
1287
1125
  throw err;
1288
1126
  }
1289
1127
  }
@@ -1304,8 +1142,9 @@ var AwsS3ObjectStore = class {
1304
1142
  ContinuationToken: options.continuationToken,
1305
1143
  MaxKeys: options.maxKeys
1306
1144
  }));
1145
+ const contents = res?.Contents ?? [];
1307
1146
  return {
1308
- objects: (res?.Contents ?? []).map((o) => ({
1147
+ objects: contents.filter((o) => typeof o.Key === "string").map((o) => ({
1309
1148
  key: o.Key,
1310
1149
  size: o.Size,
1311
1150
  etag: o.ETag,
@@ -1336,19 +1175,31 @@ var AwsS3ObjectStore = class {
1336
1175
 
1337
1176
  //#endregion
1338
1177
  //#region src/integrations/object-store/in-memory.ts
1178
+ const DEFAULT_MAX_OBJECTS = 1e4;
1179
+ const STREAM_TIMEOUT_MS = 3e4;
1339
1180
  function toBuffer(body) {
1340
1181
  if (typeof body === "string") return Buffer.from(body);
1341
1182
  if (Buffer.isBuffer(body)) return body;
1342
1183
  if (body instanceof Uint8Array) return Buffer.from(body);
1343
1184
  return new Promise((resolve, reject) => {
1344
1185
  const chunks = [];
1186
+ const timer = setTimeout(() => {
1187
+ body.destroy(new Error("InMemoryObjectStore: stream read timed out"));
1188
+ reject(new Error("InMemoryObjectStore: stream read timed out"));
1189
+ }, STREAM_TIMEOUT_MS);
1345
1190
  body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1346
- body.on("end", () => resolve(Buffer.concat(chunks)));
1347
- body.on("error", reject);
1191
+ body.on("end", () => {
1192
+ clearTimeout(timer);
1193
+ resolve(Buffer.concat(chunks));
1194
+ });
1195
+ body.on("error", (err) => {
1196
+ clearTimeout(timer);
1197
+ reject(err);
1198
+ });
1348
1199
  });
1349
1200
  }
1350
- function simpleEtag(buf) {
1351
- return `mem-${buf.length}-${buf.subarray(0, 8).toString("hex")}`;
1201
+ function computeEtag(buf) {
1202
+ return `"${createHash("md5").update(buf).digest("hex")}"`;
1352
1203
  }
1353
1204
  /**
1354
1205
  * InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
@@ -1357,6 +1208,11 @@ function simpleEtag(buf) {
1357
1208
  var InMemoryObjectStore = class {
1358
1209
  provider = "in-memory";
1359
1210
  buckets = new Map();
1211
+ maxObjects;
1212
+ objectCount = 0;
1213
+ constructor(options = {}) {
1214
+ this.maxObjects = options.maxObjects ?? DEFAULT_MAX_OBJECTS;
1215
+ }
1360
1216
  bucketMap(bucket) {
1361
1217
  let m = this.buckets.get(bucket);
1362
1218
  if (!m) {
@@ -1366,16 +1222,20 @@ var InMemoryObjectStore = class {
1366
1222
  return m;
1367
1223
  }
1368
1224
  async putObject(ref, body, options = {}) {
1225
+ const map = this.bucketMap(ref.bucket);
1226
+ const isNew = !map.has(ref.key);
1227
+ if (isNew && this.objectCount >= this.maxObjects) throw new Error(`InMemoryObjectStore: max object limit reached (${this.maxObjects})`);
1369
1228
  const buf = await toBuffer(body);
1370
1229
  const obj = {
1371
1230
  body: buf,
1372
1231
  contentType: options.contentType,
1373
1232
  cacheControl: options.cacheControl,
1374
1233
  metadata: options.metadata,
1375
- etag: simpleEtag(buf),
1234
+ etag: computeEtag(buf),
1376
1235
  lastModified: new Date()
1377
1236
  };
1378
- this.bucketMap(ref.bucket).set(ref.key, obj);
1237
+ map.set(ref.key, obj);
1238
+ if (isNew) this.objectCount++;
1379
1239
  return { etag: obj.etag };
1380
1240
  }
1381
1241
  async getObject(ref) {
@@ -1407,18 +1267,30 @@ var InMemoryObjectStore = class {
1407
1267
  };
1408
1268
  }
1409
1269
  async deleteObject(ref) {
1410
- this.bucketMap(ref.bucket).delete(ref.key);
1270
+ const map = this.bucketMap(ref.bucket);
1271
+ if (map.delete(ref.key)) this.objectCount--;
1411
1272
  }
1412
1273
  async listObjects(options) {
1413
- const { bucket, prefix = "" } = options;
1274
+ const { bucket, prefix = "", continuationToken, maxKeys = 1e3 } = options;
1414
1275
  const map = this.bucketMap(bucket);
1415
- const objects = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
1276
+ const all = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
1416
1277
  key,
1417
1278
  size: obj.body.length,
1418
1279
  etag: obj.etag,
1419
1280
  lastModified: obj.lastModified
1420
1281
  })).sort((a, b) => a.key.localeCompare(b.key));
1421
- return { objects };
1282
+ let startIndex = 0;
1283
+ if (continuationToken) {
1284
+ const idx = all.findIndex((o) => o.key === continuationToken);
1285
+ if (idx === -1) throw new Error(`InMemoryObjectStore: invalid continuationToken "${continuationToken}"`);
1286
+ startIndex = idx + 1;
1287
+ }
1288
+ const page = all.slice(startIndex, startIndex + maxKeys);
1289
+ const hasMore = startIndex + maxKeys < all.length;
1290
+ return {
1291
+ objects: page,
1292
+ nextContinuationToken: hasMore ? page[page.length - 1]?.key : void 0
1293
+ };
1422
1294
  }
1423
1295
  async presignGetObject(_ref, _options) {
1424
1296
  throw new Error("InMemoryObjectStore does not support presigned URLs");