@ckirg/corelib 0.1.22

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.
@@ -0,0 +1,876 @@
1
+ import {
2
+ existsSync,
3
+ getAllEnv,
4
+ getCwd,
5
+ getDirname,
6
+ getMode,
7
+ getPlatform,
8
+ getRequire,
9
+ loggers_default,
10
+ readTextFileSync
11
+ } from "./chunk-BAVE2JXI.js";
12
+ import {
13
+ nextCid
14
+ } from "./chunk-2DF4ADYX.js";
15
+ import {
16
+ detectRuntime
17
+ } from "./chunk-HPE2XSTW.js";
18
+
19
+ // src/retrieve/RequestUnlimited.ts
20
+ import { deepmergeCustom as deepmergeCustom2 } from "deepmerge-ts";
21
+ import ky, { HTTPError } from "ky";
22
+ import { serializeError as serializeError3 } from "serialize-error";
23
+
24
+ // src/configs/ConfigManager.ts
25
+ import { EventEmitter } from "events";
26
+ import { deepmergeCustom } from "deepmerge-ts";
27
+ import { serializeError } from "serialize-error";
28
+
29
+ // src/configs/ConfigManager.json
30
+ var ConfigManager_default = {
31
+ markets: {
32
+ chromeVersion: "146",
33
+ nasdaq: {
34
+ statusEndpoint: "https://api.nasdaq.com/api/market-info",
35
+ monitor: {
36
+ liveIntervalSec: 10,
37
+ closedIntervalSec: 3600,
38
+ warnIntervalSec: 60
39
+ },
40
+ symbols: {
41
+ nasdaqListedUrl: "https://www.nasdaqtrader.com/dynamic/symdir/nasdaqlisted.txt",
42
+ otherListedUrl: "https://www.nasdaqtrader.com/dynamic/symdir/otherlisted.txt",
43
+ initialBackoffMs: 1e3,
44
+ maxRetryBackoffMs: 36e5,
45
+ maxFetchRetries: 10
46
+ },
47
+ quotes: {
48
+ concurrencyLimit: 5
49
+ }
50
+ }
51
+ },
52
+ retrieve: {
53
+ timeout: 5e4,
54
+ retry: {
55
+ limit: 5,
56
+ backoffLimit: 3e3
57
+ }
58
+ }
59
+ };
60
+
61
+ // src/configs/ConfigUtils.ts
62
+ async function decryptConfig(encryptedData) {
63
+ const { getEnv } = await import("./utils-Q4C2EEPD.js");
64
+ const crypto = (await import("crypto")).default;
65
+ const password = getEnv("CORELIB_AES_PASSWORD");
66
+ if (!password) {
67
+ throw new Error(
68
+ "Decryption failed: CORELIB_AES_PASSWORD environment variable is not set."
69
+ );
70
+ }
71
+ const lines = encryptedData.split("\n").map((l) => l.trim()).filter(Boolean);
72
+ if (lines.length < 2) {
73
+ throw new Error(
74
+ "Invalid .enc file format. Expected IV on line 1 and Ciphertext on line 2."
75
+ );
76
+ }
77
+ const iv = Buffer.from(lines[0], "base64");
78
+ const ciphertext = Buffer.from(lines[1], "base64");
79
+ const key = Buffer.from(password, "hex");
80
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
81
+ let decrypted = decipher.update(ciphertext);
82
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
83
+ return JSON.parse(decrypted.toString("utf8"));
84
+ }
85
+
86
+ // src/configs/ConfigManager.ts
87
+ var leafMerger = deepmergeCustom({
88
+ mergeArrays: false
89
+ });
90
+ var isSafeKey = (key) => !key.split(/[.-]/).some((p) => p === "__proto__" || p === "constructor" || p === "prototype");
91
+ function clearAndFill(target, source) {
92
+ for (const key of Object.keys(target)) {
93
+ if (!(key in source)) delete target[key];
94
+ }
95
+ for (const key of Object.keys(source)) {
96
+ const val = source[key];
97
+ if (val && typeof val === "object" && !Array.isArray(val)) {
98
+ if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
99
+ target[key] = {};
100
+ }
101
+ clearAndFill(
102
+ target[key],
103
+ val
104
+ );
105
+ } else {
106
+ target[key] = val;
107
+ }
108
+ }
109
+ }
110
+ function resolveAppName(cwd, pathMod, fileExists, readFile) {
111
+ let dir = cwd;
112
+ for (; ; ) {
113
+ if (fileExists(pathMod.join(dir, "pnpm-workspace.yaml")))
114
+ return pathMod.basename(dir);
115
+ const pkgPath = pathMod.join(dir, "package.json");
116
+ if (fileExists(pkgPath)) {
117
+ try {
118
+ const pkg = JSON.parse(readFile(pkgPath));
119
+ if (pkg.workspaces) return pathMod.basename(dir);
120
+ } catch {
121
+ }
122
+ }
123
+ const parent = pathMod.dirname(dir);
124
+ if (parent === dir) break;
125
+ dir = parent;
126
+ }
127
+ return pathMod.basename(cwd);
128
+ }
129
+ var ConfigManager = class _ConfigManager extends EventEmitter {
130
+ static instance;
131
+ _config = {};
132
+ _defaultsPath;
133
+ static _logger = loggers_default.child({ section: "ConfigManager" });
134
+ /** Single-flight guard for initialize(); null until first call, evicted on failure. */
135
+ _initPromise = null;
136
+ _isInitialized = false;
137
+ /** Async mutex: every async mutator chains onto this so they never interleave. */
138
+ _mutationChain = Promise.resolve();
139
+ /** The staging object of an in-flight staged build, or null. Lets a synchronous
140
+ * updateValue() between awaits survive the final clearAndFill swap. */
141
+ _inFlightTempConfig = null;
142
+ /** Throttle: emit the premature-read warning at most once per process (get()
143
+ * is called pervasively, so a per-call warn would flood dev logs). */
144
+ static _prematureWarnEmitted = false;
145
+ constructor() {
146
+ super();
147
+ const __dirname = getDirname();
148
+ let defaultsPath = __dirname;
149
+ try {
150
+ const { join } = getRequire()("node:path");
151
+ defaultsPath = join(__dirname, "ConfigManager.json");
152
+ } catch (_e) {
153
+ defaultsPath = `${__dirname}/ConfigManager.json`;
154
+ }
155
+ this._defaultsPath = defaultsPath;
156
+ clearAndFill(
157
+ this._config,
158
+ ConfigManager_default
159
+ );
160
+ globalThis.sysconfig = this._config;
161
+ }
162
+ /**
163
+ * Singleton accessor for the ConfigManager
164
+ */
165
+ static getInstance() {
166
+ if (!_ConfigManager.instance) {
167
+ _ConfigManager.instance = new _ConfigManager();
168
+ }
169
+ return _ConfigManager.instance;
170
+ }
171
+ /**
172
+ * Retrieves a nested configuration value by string path (e.g., "db.mysql.port").
173
+ * @param {string} path - The dot-notation path to the configuration value.
174
+ * @returns The value at the specified path, or undefined if not found.
175
+ */
176
+ get(path) {
177
+ if (!this._isInitialized && !_ConfigManager._prematureWarnEmitted && getMode() !== "production") {
178
+ _ConfigManager._prematureWarnEmitted = true;
179
+ _ConfigManager._logger.warn(
180
+ `ConfigManager.get("${path}") called before initialize() resolved; returning seeded default`
181
+ );
182
+ }
183
+ const keys = path.split(".");
184
+ let current = this._config;
185
+ for (const key of keys) {
186
+ if (current === null || typeof current !== "object" || !(key in current)) {
187
+ return void 0;
188
+ }
189
+ current = current[key];
190
+ }
191
+ return current;
192
+ }
193
+ /**
194
+ * Static helper to retrieve a configuration value from the singleton instance.
195
+ */
196
+ static get(path) {
197
+ return _ConfigManager.getInstance().get(path);
198
+ }
199
+ /**
200
+ * Idempotent under concurrency: simultaneous calls share one in-flight
201
+ * promise (-01). On failure the promise is evicted so a transient error can
202
+ * self-heal on a later call. Signature unchanged (still awaitable Promise<void>).
203
+ */
204
+ initialize(args) {
205
+ if (this._initPromise) return this._initPromise;
206
+ this._initPromise = this._enqueue(() => this.runInitSequence(args)).then(
207
+ () => {
208
+ this._isInitialized = true;
209
+ this._initPromise = null;
210
+ },
211
+ (error) => {
212
+ this._initPromise = null;
213
+ throw error;
214
+ }
215
+ );
216
+ return this._initPromise;
217
+ }
218
+ /**
219
+ * Main initialization sequence (formerly the body of initialize()).
220
+ * 1. Load Defaults 2. Detect CLI -C 3. Process Hierarchy 4. Env 5. CLI overrides.
221
+ */
222
+ async runInitSequence(args) {
223
+ _ConfigManager._logger.trace("initialize: start");
224
+ const argv = args ?? (typeof process !== "undefined" && Array.isArray(process.argv) ? process.argv.slice(2) : []);
225
+ let configPath;
226
+ const overrides = {};
227
+ for (let i = 0; i < argv.length; i++) {
228
+ const tok = argv[i];
229
+ if (tok === "-C" || tok === "--config") {
230
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("-"))
231
+ configPath = argv[++i];
232
+ continue;
233
+ }
234
+ if (tok.startsWith("--config=")) {
235
+ configPath = tok.slice("--config=".length);
236
+ continue;
237
+ }
238
+ if (!tok.startsWith("--")) continue;
239
+ let key = tok.slice(2);
240
+ if (key === "" || key.startsWith("=")) continue;
241
+ let value;
242
+ const eq = key.indexOf("=");
243
+ if (eq > -1) {
244
+ value = key.slice(eq + 1);
245
+ key = key.slice(0, eq);
246
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
247
+ value = argv[++i];
248
+ } else {
249
+ value = true;
250
+ }
251
+ overrides[key] = value;
252
+ }
253
+ _ConfigManager._logger.trace("initialize: resolved", {
254
+ hasConfigPath: configPath != null,
255
+ overrideCount: Object.keys(overrides).length
256
+ });
257
+ const tempConfig = {};
258
+ this._inFlightTempConfig = tempConfig;
259
+ try {
260
+ this.loadDefaults(tempConfig);
261
+ if (configPath) {
262
+ const externalData = await this.fetchExternalConfig(configPath);
263
+ this.processHierarchy(externalData, tempConfig);
264
+ }
265
+ this.applyEnvOverrides(tempConfig);
266
+ this.applyCliOverrides(overrides, tempConfig);
267
+ clearAndFill(this._config, tempConfig);
268
+ _ConfigManager._logger.trace("initialize: committed", {
269
+ keys: Object.keys(this._config ?? {}).length
270
+ });
271
+ this.emit("initialized", this._config);
272
+ } finally {
273
+ this._inFlightTempConfig = null;
274
+ }
275
+ }
276
+ /**
277
+ * Serializes an async mutator against all other mutators (initialize +
278
+ * loadExternalConfig) via _mutationChain. The work runs after the previous
279
+ * op settles (success OR failure — `.then(work, work)` so a prior rejection
280
+ * never wedges the chain). The chain itself swallows results/errors so it
281
+ * stays resolved; the caller still receives this op's real result/error.
282
+ *
283
+ * Note: the chain's swallow-handler means a `loadExternalConfig` rejection is
284
+ * absorbed if the caller discards the returned promise. `initialize()` is
285
+ * different — it wraps this result in `_initPromise`, which is NOT on the
286
+ * chain, so an un-awaited failed `initialize()` surfaces as an unhandled
287
+ * rejection in Node strict mode. Callers should await/catch both.
288
+ */
289
+ _enqueue(work) {
290
+ const run = this._mutationChain.then(work, work);
291
+ this._mutationChain = run.then(
292
+ () => void 0,
293
+ () => void 0
294
+ );
295
+ return run;
296
+ }
297
+ /**
298
+ * Retrieves the current active configuration object.
299
+ */
300
+ getConfig() {
301
+ return this._config;
302
+ }
303
+ /** True once a call to initialize() has settled successfully. */
304
+ get isInitialized() {
305
+ return this._isInitialized;
306
+ }
307
+ /**
308
+ * Resolves when the first initialize() settles successfully (rejects if that
309
+ * in-flight initialize fails). If initialize() was never started — OR a prior
310
+ * attempt failed and was evicted — this resolves immediately and does NOT
311
+ * imply the manager is initialized; callers needing guaranteed initialization
312
+ * must call initialize() first and catch its rejection.
313
+ */
314
+ whenReady() {
315
+ if (this._isInitialized) return Promise.resolve();
316
+ return this._initPromise ?? Promise.resolve();
317
+ }
318
+ /**
319
+ * Public method to load and merge a new configuration from a URL or file path on demand.
320
+ * Respects the established configuration hierarchy and maintains Env overrides.
321
+ * @param source - The URL or local file path to the configuration.
322
+ */
323
+ loadExternalConfig(source) {
324
+ return this._enqueue(() => this.loadExternalConfigInner(source));
325
+ }
326
+ async loadExternalConfigInner(source) {
327
+ try {
328
+ const externalData = await this.fetchExternalConfig(source);
329
+ const tempConfig = structuredClone(this._config);
330
+ this._inFlightTempConfig = tempConfig;
331
+ try {
332
+ this.processHierarchy(externalData, tempConfig);
333
+ this.applyEnvOverrides(tempConfig);
334
+ clearAndFill(this._config, tempConfig);
335
+ this.emit("configLoaded", this._config);
336
+ } finally {
337
+ this._inFlightTempConfig = null;
338
+ }
339
+ } catch (error) {
340
+ this.logError(
341
+ `Failed to load external config dynamically from ${source}`,
342
+ error
343
+ );
344
+ throw error;
345
+ }
346
+ }
347
+ /**
348
+ * Loads the base ConfigManager.json from the local directory.
349
+ * Always seeds from the bundled JSON (available in all runtimes, including edge).
350
+ * If the JSON file is also found on disk, it replaces the bundled defaults.
351
+ */
352
+ loadDefaults(target) {
353
+ let defaults = {
354
+ ...ConfigManager_default
355
+ };
356
+ if (existsSync(this._defaultsPath)) {
357
+ try {
358
+ defaults = JSON.parse(readTextFileSync(this._defaultsPath));
359
+ } catch (e) {
360
+ this.logError("Failed to load defaults", e);
361
+ }
362
+ }
363
+ clearAndFill(target, defaults);
364
+ }
365
+ /**
366
+ * Fetches and parses configuration from a URL or Local Path.
367
+ * Supports .enc decryption and dynamic confbox parsing by extension.
368
+ */
369
+ async fetchExternalConfig(source) {
370
+ _ConfigManager._logger.trace("external config", {
371
+ source: source.startsWith("http") ? "http" : "file"
372
+ });
373
+ let content;
374
+ if (source.startsWith("http")) {
375
+ const { endPoint: endPoint2 } = await import("./RequestUnlimited-CLB36IJX.js");
376
+ const result = await endPoint2(source);
377
+ if (result.status === "error") {
378
+ throw new Error("Failed to fetch external config");
379
+ }
380
+ content = result.value.body;
381
+ } else {
382
+ content = readTextFileSync(source);
383
+ }
384
+ const lowerSource = source.toLowerCase();
385
+ if (lowerSource.endsWith(".enc")) {
386
+ _ConfigManager._logger.trace("decrypting .enc config");
387
+ const decrypted = this.validateConfigObject(
388
+ await decryptConfig(content),
389
+ source
390
+ );
391
+ _ConfigManager._logger.trace("decryption ok");
392
+ return decrypted;
393
+ }
394
+ const confbox = await import("confbox");
395
+ if (lowerSource.endsWith(".yaml") || lowerSource.endsWith(".yml")) {
396
+ return this.validateConfigObject(confbox.parseYAML(content), source);
397
+ }
398
+ if (lowerSource.endsWith(".toml")) {
399
+ return this.validateConfigObject(confbox.parseTOML(content), source);
400
+ }
401
+ if (lowerSource.endsWith(".json5")) {
402
+ return this.validateConfigObject(confbox.parseJSON5(content), source);
403
+ }
404
+ if (lowerSource.endsWith(".jsonc")) {
405
+ return this.validateConfigObject(confbox.parseJSONC(content), source);
406
+ }
407
+ if (lowerSource.endsWith(".ini")) {
408
+ return this.validateConfigObject(confbox.parseINI(content), source);
409
+ }
410
+ return this.validateConfigObject(confbox.parseJSON(content), source);
411
+ }
412
+ validateConfigObject(parsed, source) {
413
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
414
+ throw new Error(
415
+ `Config from "${source}" must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`
416
+ );
417
+ }
418
+ return parsed;
419
+ }
420
+ /**
421
+ * Processes the specific hierarchy:
422
+ * commonAll -> [AppName].common -> [AppName].[platform] -> [AppName].[platform].[mode]
423
+ */
424
+ processHierarchy(data, target) {
425
+ if (!data) return;
426
+ const appName = this.getAppName();
427
+ const platform = getPlatform();
428
+ const mode = getMode();
429
+ let layeredConfig = data.commonAll || {};
430
+ const appKey = Object.keys(data).find(
431
+ (k) => k.toLowerCase() === appName.toLowerCase()
432
+ );
433
+ const appSection = appKey ? data[appKey] : null;
434
+ if (appSection) {
435
+ if (appSection.common) {
436
+ layeredConfig = leafMerger(
437
+ layeredConfig,
438
+ appSection.common
439
+ );
440
+ }
441
+ const platformSection = appSection[platform];
442
+ if (platformSection) {
443
+ const modeSection = platformSection[mode];
444
+ if (modeSection) {
445
+ layeredConfig = leafMerger(layeredConfig, modeSection);
446
+ }
447
+ }
448
+ }
449
+ const merged = leafMerger(target, layeredConfig);
450
+ clearAndFill(target, merged);
451
+ }
452
+ /**
453
+ * Maps CORELIB_ prefixed environment variables to config keys.
454
+ * Example: CORELIB_DB_PORT -> config.db.port
455
+ */
456
+ applyEnvOverrides(target) {
457
+ const prefix = "CORELIB_";
458
+ const env = getAllEnv();
459
+ Object.keys(env).forEach((envKey) => {
460
+ if (envKey.startsWith(prefix)) {
461
+ const configPath = envKey.slice(prefix.length).toLowerCase().replace(/_/g, ".");
462
+ _ConfigManager._logger.trace("env override", { key: configPath });
463
+ const value = this.parseValue(env[envKey]);
464
+ this.setPath(target, configPath, value);
465
+ }
466
+ });
467
+ }
468
+ /**
469
+ * Maps the parsed Kebab-case CLI overrides to the config structure.
470
+ * Unsafe keys (__proto__/constructor/prototype segments) are dropped.
471
+ */
472
+ applyCliOverrides(overrides, target) {
473
+ Object.keys(overrides).forEach((key) => {
474
+ if (key === "config") return;
475
+ if (!isSafeKey(key)) {
476
+ _ConfigManager._logger.warn(
477
+ `Dropped unsafe CLI override key "${key}" (prototype-pollution guard)`
478
+ );
479
+ return;
480
+ }
481
+ const configPath = key.replace(/-/g, ".");
482
+ _ConfigManager._logger.trace("cli override", { key: configPath });
483
+ const value = this.parseValue(overrides[key]);
484
+ this.setPath(target, configPath, value);
485
+ });
486
+ }
487
+ /**
488
+ * Core update method that updates both the local object
489
+ * and the active globalThis object, then emits events.
490
+ */
491
+ updateValue(path, value) {
492
+ this.setPath(this._config, path, value);
493
+ if (this._inFlightTempConfig !== null) {
494
+ this.setPath(this._inFlightTempConfig, path, value);
495
+ }
496
+ this.emit("change", { path, value });
497
+ this.emit(`change:${path}`, value);
498
+ }
499
+ /**
500
+ * Helper to set nested object values by string path (e.g., "db.mysql.port")
501
+ */
502
+ setPath(obj, path, value) {
503
+ const keys = path.split(".");
504
+ let current = obj;
505
+ while (keys.length > 1) {
506
+ const key = keys.shift();
507
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
508
+ current[key] = {};
509
+ }
510
+ current = current[key];
511
+ }
512
+ current[keys[0]] = value;
513
+ }
514
+ /**
515
+ * Parses values from Env/CLI, automatically handling JSON strings for arrays/objects.
516
+ */
517
+ parseValue(val) {
518
+ if (typeof val !== "string") return val;
519
+ if (val.startsWith("[") && val.endsWith("]") || val.startsWith("{") && val.endsWith("}")) {
520
+ try {
521
+ return JSON.parse(val);
522
+ } catch (e) {
523
+ this.logError("Failed to parse complex JSON from CLI/Env flag", e);
524
+ return val;
525
+ }
526
+ }
527
+ if (val.toLowerCase() === "true") return true;
528
+ if (val.toLowerCase() === "false") return false;
529
+ if (!Number.isNaN(Number(val)) && val.trim() !== "") return Number(val);
530
+ return val;
531
+ }
532
+ getAppName() {
533
+ try {
534
+ const runtime = detectRuntime();
535
+ if (runtime === "node" || runtime === "bun" || typeof import.meta !== "undefined" && import.meta.url && import.meta.url.startsWith("file:")) {
536
+ const path = getRequire()("node:path");
537
+ return resolveAppName(
538
+ getCwd(),
539
+ path,
540
+ (p) => existsSync(p),
541
+ (p) => readTextFileSync(p)
542
+ );
543
+ }
544
+ return "edge-app";
545
+ } catch (e) {
546
+ this.logError(
547
+ "Failed to get app name from cwd. Falling back to default-app",
548
+ e
549
+ );
550
+ return "default-app";
551
+ }
552
+ }
553
+ /**
554
+ * Logs errors internally. If the global pino logger is available, it uses it
555
+ * along with `serialize-error` to structure the error object for Vector sidecars.
556
+ */
557
+ logError(msg, error) {
558
+ const serialized = error ? serializeError(error) : void 0;
559
+ _ConfigManager._logger.error(msg, { error: serialized });
560
+ }
561
+ // --- Rust Integration Helpers ---
562
+ toJsonString() {
563
+ return JSON.stringify(this._config);
564
+ }
565
+ toBuffer() {
566
+ return Buffer.from(this.toJsonString());
567
+ }
568
+ /**
569
+ * Test-only: reset init/concurrency state so single-flight and failed-init
570
+ * eviction can be exercised on the singleton. Does NOT clear _config.
571
+ */
572
+ __resetForTests() {
573
+ this._initPromise = null;
574
+ this._isInitialized = false;
575
+ this._inFlightTempConfig = null;
576
+ this._mutationChain = Promise.resolve();
577
+ _ConfigManager._prematureWarnEmitted = false;
578
+ }
579
+ };
580
+
581
+ // src/retrieve/RequestResponseSerialize.ts
582
+ import { serializeError as serializeError2 } from "serialize-error";
583
+ var requestResponseSerializeLogger = loggers_default.child({
584
+ section: "RequestResponseSerialize"
585
+ });
586
+ async function serializeResponse(response) {
587
+ if (!response) return null;
588
+ const headers = {};
589
+ response.headers.forEach((v, k) => {
590
+ headers[k.toLowerCase()] = v;
591
+ });
592
+ let body;
593
+ const contentType = response.headers.get("content-type") || "";
594
+ try {
595
+ if (response.bodyUsed) {
596
+ body = "[Body already consumed]";
597
+ } else {
598
+ const rawText = await response.clone().text();
599
+ if (contentType.includes("application/json")) {
600
+ try {
601
+ body = JSON.parse(rawText);
602
+ } catch {
603
+ body = rawText;
604
+ }
605
+ } else {
606
+ body = rawText;
607
+ }
608
+ }
609
+ } catch (error) {
610
+ requestResponseSerializeLogger.warn("Failed to read response body", {
611
+ status: response.status,
612
+ url: response.url,
613
+ bodyUsed: response.bodyUsed,
614
+ error: serializeError2(error)
615
+ });
616
+ body = "[Error reading body]";
617
+ }
618
+ return {
619
+ ok: response.ok,
620
+ status: response.status,
621
+ statusText: response.statusText,
622
+ headers,
623
+ url: response.url,
624
+ redirected: response.redirected,
625
+ type: response.type,
626
+ body
627
+ };
628
+ }
629
+ var RequestResponseSerialize = {
630
+ serialize: serializeResponse
631
+ };
632
+
633
+ // src/retrieve/RequestUnlimited.ts
634
+ var requestUnlimitedLogger = loggers_default.child({ section: "RequestUnlimited" });
635
+ var customDeepmerge = deepmergeCustom2({
636
+ mergeArrays: false
637
+ });
638
+ var MAX_RETRY_LIMIT = 10;
639
+ var MIN_TIMEOUT_MS = 1e3;
640
+ var MAX_TIMEOUT_MS = 12e4;
641
+ var MAX_BACKOFF_LIMIT_MS = 6e4;
642
+ function clampNumber(value, min, max, fallback) {
643
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
644
+ return Math.min(max, Math.max(min, value));
645
+ }
646
+ function safeUrl(u) {
647
+ try {
648
+ const parsed = u instanceof URL ? u : new URL(u instanceof Request ? u.url : u);
649
+ return `${parsed.origin}${parsed.pathname}`;
650
+ } catch {
651
+ return "[unparseable url]";
652
+ }
653
+ }
654
+ function fullJitterDelay(attempt, backoffLimit) {
655
+ const base = Math.min(backoffLimit, 300 * 2 ** (attempt - 1));
656
+ const delayMs = Math.round(Math.random() * base);
657
+ requestUnlimitedLogger.trace("retry delay computed", {
658
+ attempt,
659
+ base,
660
+ cap: backoffLimit,
661
+ delayMs
662
+ });
663
+ return delayMs;
664
+ }
665
+ var DEFAULT_REQUEST_OPTIONS = {
666
+ timeout: 5e4,
667
+ throwHttpErrors: true,
668
+ retry: {
669
+ limit: 5,
670
+ methods: ["get", "post", "put", "delete", "patch"],
671
+ backoffLimit: 3e3,
672
+ shouldRetry: ({ error, retryCount }) => {
673
+ if (error instanceof HTTPError && error.response) {
674
+ const status = error.response.status;
675
+ if (status === 429 && retryCount <= 5) {
676
+ requestUnlimitedLogger.trace("retry decision", {
677
+ status,
678
+ willRetry: true
679
+ });
680
+ return true;
681
+ }
682
+ if (status >= 400 && status < 500) {
683
+ requestUnlimitedLogger.trace("retry decision: skip", {
684
+ status,
685
+ willRetry: false
686
+ });
687
+ return false;
688
+ }
689
+ const willRetry = status >= 500;
690
+ requestUnlimitedLogger.trace(
691
+ willRetry ? "retry decision" : "retry decision: skip",
692
+ { status, willRetry }
693
+ );
694
+ return willRetry;
695
+ }
696
+ requestUnlimitedLogger.trace("retry decision", { willRetry: true });
697
+ return true;
698
+ }
699
+ },
700
+ method: "get",
701
+ headers: {
702
+ "content-type": "application/json",
703
+ accept: "application/json"
704
+ },
705
+ hooks: {
706
+ // Retry logging is done per-call in endPoint() so it can carry the request's cid;
707
+ // a module-level default hook here has no access to that per-call correlation id.
708
+ beforeRetry: []
709
+ }
710
+ };
711
+ function toLowercaseKeys(obj) {
712
+ const newObj = {};
713
+ for (const key in obj) {
714
+ if (Object.hasOwn(obj, key) && obj[key] !== void 0) {
715
+ newObj[key.toLowerCase()] = obj[key];
716
+ }
717
+ }
718
+ return newObj;
719
+ }
720
+ async function endPoint(url, options = {}) {
721
+ const normalizedDefaultHeaders = toLowercaseKeys(
722
+ DEFAULT_REQUEST_OPTIONS.headers || {}
723
+ );
724
+ const normalizedInputHeaders = options.headers ? toLowercaseKeys(options.headers) : {};
725
+ const { headers, hooks, ...remainingOptions } = options;
726
+ const cfgTimeout = clampNumber(
727
+ ConfigManager.get("retrieve.timeout"),
728
+ MIN_TIMEOUT_MS,
729
+ MAX_TIMEOUT_MS,
730
+ 5e4
731
+ );
732
+ const cfgRetryLimit = clampNumber(
733
+ ConfigManager.get("retrieve.retry.limit"),
734
+ 0,
735
+ MAX_RETRY_LIMIT,
736
+ 5
737
+ );
738
+ const cfgBackoffLimit = clampNumber(
739
+ ConfigManager.get("retrieve.retry.backoffLimit"),
740
+ 0,
741
+ MAX_BACKOFF_LIMIT_MS,
742
+ 3e3
743
+ );
744
+ const cid = nextCid();
745
+ const startedAt = performance.now();
746
+ requestUnlimitedLogger.trace("endPoint: request", {
747
+ cid,
748
+ url: safeUrl(url),
749
+ timeout: cfgTimeout,
750
+ retryLimit: cfgRetryLimit,
751
+ backoffLimit: cfgBackoffLimit
752
+ });
753
+ const defaultRetry = DEFAULT_REQUEST_OPTIONS.retry;
754
+ const kyOptions = customDeepmerge(
755
+ DEFAULT_REQUEST_OPTIONS,
756
+ {
757
+ timeout: cfgTimeout,
758
+ retry: {
759
+ ...defaultRetry,
760
+ limit: cfgRetryLimit,
761
+ backoffLimit: cfgBackoffLimit,
762
+ delay: (attempt) => fullJitterDelay(attempt, cfgBackoffLimit)
763
+ }
764
+ },
765
+ remainingOptions,
766
+ {
767
+ headers: { ...normalizedDefaultHeaders, ...normalizedInputHeaders },
768
+ hooks: {
769
+ beforeRetry: [
770
+ ...DEFAULT_REQUEST_OPTIONS.hooks?.beforeRetry || [],
771
+ // Per-call so the retry trace correlates to this request's cid.
772
+ async ({ retryCount }) => {
773
+ requestUnlimitedLogger.trace("endPoint: retry", {
774
+ cid,
775
+ retryCount,
776
+ durationMs: performance.now() - startedAt
777
+ });
778
+ },
779
+ ...hooks?.beforeRetry || []
780
+ ]
781
+ }
782
+ }
783
+ );
784
+ try {
785
+ const responseObject = await ky(url, kyOptions);
786
+ const response = await serializeResponse(responseObject);
787
+ requestUnlimitedLogger.trace("endPoint: ok", {
788
+ cid,
789
+ durationMs: performance.now() - startedAt,
790
+ url: safeUrl(url),
791
+ status: responseObject.status
792
+ });
793
+ return {
794
+ status: "success",
795
+ value: response
796
+ };
797
+ } catch (error) {
798
+ if (error instanceof HTTPError || error.response) {
799
+ const errorResponse = await serializeResponse(
800
+ // @ts-expect-error - ky error property
801
+ error.response
802
+ );
803
+ requestUnlimitedLogger.trace("endPoint: error", {
804
+ cid,
805
+ durationMs: performance.now() - startedAt,
806
+ status: errorResponse?.status,
807
+ errorMsg: "HTTP error"
808
+ });
809
+ requestUnlimitedLogger.warn("HTTP Error", {
810
+ cid,
811
+ status: errorResponse?.status,
812
+ url: url.toString()
813
+ });
814
+ return {
815
+ status: "error",
816
+ reason: errorResponse
817
+ };
818
+ }
819
+ const serializedError = serializeError3(error);
820
+ requestUnlimitedLogger.trace("endPoint: error", {
821
+ cid,
822
+ durationMs: performance.now() - startedAt,
823
+ errorMsg: serializedError?.message ?? "internal/network error"
824
+ });
825
+ requestUnlimitedLogger.error("Internal/Network Error", {
826
+ cid,
827
+ error: serializedError
828
+ });
829
+ return {
830
+ status: "error",
831
+ reason: serializedError
832
+ };
833
+ }
834
+ }
835
+ async function endPoints(urls, options = {}) {
836
+ const cid = nextCid();
837
+ const startedAt = performance.now();
838
+ requestUnlimitedLogger.trace("endPoints: batch", { cid, count: urls.length });
839
+ const promises = urls.map((url) => endPoint(url, options));
840
+ const results = await Promise.allSettled(promises);
841
+ const mapped = results.map((result) => {
842
+ if (result.status === "fulfilled") return result.value;
843
+ return {
844
+ status: "error",
845
+ reason: serializeError3(result.reason)
846
+ };
847
+ });
848
+ const ok = mapped.filter((r) => r.status === "success").length;
849
+ requestUnlimitedLogger.trace("endPoints: done", {
850
+ cid,
851
+ durationMs: performance.now() - startedAt,
852
+ ok,
853
+ failed: mapped.length - ok
854
+ });
855
+ return mapped;
856
+ }
857
+ var RequestUnlimited = {
858
+ defaults: DEFAULT_REQUEST_OPTIONS,
859
+ endPoint,
860
+ endPoints
861
+ };
862
+
863
+ export {
864
+ RequestResponseSerialize,
865
+ MAX_RETRY_LIMIT,
866
+ MIN_TIMEOUT_MS,
867
+ MAX_TIMEOUT_MS,
868
+ MAX_BACKOFF_LIMIT_MS,
869
+ clampNumber,
870
+ fullJitterDelay,
871
+ DEFAULT_REQUEST_OPTIONS,
872
+ endPoint,
873
+ endPoints,
874
+ RequestUnlimited,
875
+ ConfigManager
876
+ };