@digital-alchemy/core 26.1.9 → 26.5.1

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.
Files changed (85) hide show
  1. package/CLAUDE.md +302 -0
  2. package/README.md +19 -3
  3. package/dist/helpers/async.d.mts +37 -0
  4. package/dist/helpers/async.mjs +50 -15
  5. package/dist/helpers/async.mjs.map +1 -1
  6. package/dist/helpers/config-environment-loader.d.mts +39 -0
  7. package/dist/helpers/config-environment-loader.mjs +51 -11
  8. package/dist/helpers/config-environment-loader.mjs.map +1 -1
  9. package/dist/helpers/config-file-loader.d.mts +66 -1
  10. package/dist/helpers/config-file-loader.mjs +82 -6
  11. package/dist/helpers/config-file-loader.mjs.map +1 -1
  12. package/dist/helpers/config.d.mts +202 -5
  13. package/dist/helpers/config.mjs +60 -0
  14. package/dist/helpers/config.mjs.map +1 -1
  15. package/dist/helpers/context.d.mts +12 -1
  16. package/dist/helpers/cron.d.mts +154 -7
  17. package/dist/helpers/cron.mjs +47 -4
  18. package/dist/helpers/cron.mjs.map +1 -1
  19. package/dist/helpers/errors.d.mts +45 -0
  20. package/dist/helpers/errors.mjs +45 -0
  21. package/dist/helpers/errors.mjs.map +1 -1
  22. package/dist/helpers/events.d.mts +23 -0
  23. package/dist/helpers/events.mjs +23 -0
  24. package/dist/helpers/events.mjs.map +1 -1
  25. package/dist/helpers/extend.d.mts +50 -0
  26. package/dist/helpers/extend.mjs +63 -0
  27. package/dist/helpers/extend.mjs.map +1 -1
  28. package/dist/helpers/index.d.mts +9 -0
  29. package/dist/helpers/index.mjs +9 -0
  30. package/dist/helpers/index.mjs.map +1 -1
  31. package/dist/helpers/lifecycle.d.mts +102 -16
  32. package/dist/helpers/lifecycle.mjs +19 -1
  33. package/dist/helpers/lifecycle.mjs.map +1 -1
  34. package/dist/helpers/logger.d.mts +178 -17
  35. package/dist/helpers/logger.mjs +41 -1
  36. package/dist/helpers/logger.mjs.map +1 -1
  37. package/dist/helpers/module.d.mts +110 -0
  38. package/dist/helpers/module.mjs +55 -6
  39. package/dist/helpers/module.mjs.map +1 -1
  40. package/dist/helpers/service-runner.d.mts +27 -1
  41. package/dist/helpers/service-runner.mjs +27 -1
  42. package/dist/helpers/service-runner.mjs.map +1 -1
  43. package/dist/helpers/utilities.d.mts +123 -3
  44. package/dist/helpers/utilities.mjs +110 -3
  45. package/dist/helpers/utilities.mjs.map +1 -1
  46. package/dist/helpers/wiring.d.mts +385 -0
  47. package/dist/helpers/wiring.mjs +120 -0
  48. package/dist/helpers/wiring.mjs.map +1 -1
  49. package/dist/services/als.service.d.mts +10 -0
  50. package/dist/services/als.service.mjs +49 -0
  51. package/dist/services/als.service.mjs.map +1 -1
  52. package/dist/services/configuration.service.d.mts +22 -0
  53. package/dist/services/configuration.service.mjs +140 -12
  54. package/dist/services/configuration.service.mjs.map +1 -1
  55. package/dist/services/index.d.mts +8 -0
  56. package/dist/services/index.mjs +8 -0
  57. package/dist/services/index.mjs.map +1 -1
  58. package/dist/services/internal.service.d.mts +98 -19
  59. package/dist/services/internal.service.mjs +91 -9
  60. package/dist/services/internal.service.mjs.map +1 -1
  61. package/dist/services/is.service.d.mts +64 -4
  62. package/dist/services/is.service.mjs +67 -4
  63. package/dist/services/is.service.mjs.map +1 -1
  64. package/dist/services/lifecycle.service.d.mts +26 -0
  65. package/dist/services/lifecycle.service.mjs +67 -9
  66. package/dist/services/lifecycle.service.mjs.map +1 -1
  67. package/dist/services/logger.service.d.mts +27 -0
  68. package/dist/services/logger.service.mjs +133 -9
  69. package/dist/services/logger.service.mjs.map +1 -1
  70. package/dist/services/scheduler.service.d.mts +19 -0
  71. package/dist/services/scheduler.service.mjs +87 -4
  72. package/dist/services/scheduler.service.mjs.map +1 -1
  73. package/dist/services/wiring.service.d.mts +29 -1
  74. package/dist/services/wiring.service.mjs +153 -20
  75. package/dist/services/wiring.service.mjs.map +1 -1
  76. package/dist/testing/index.d.mts +4 -0
  77. package/dist/testing/index.mjs +4 -0
  78. package/dist/testing/index.mjs.map +1 -1
  79. package/dist/testing/mock-logger.d.mts +8 -0
  80. package/dist/testing/mock-logger.mjs +9 -0
  81. package/dist/testing/mock-logger.mjs.map +1 -1
  82. package/dist/testing/test-module.d.mts +107 -27
  83. package/dist/testing/test-module.mjs +58 -1
  84. package/dist/testing/test-module.mjs.map +1 -1
  85. package/package.json +33 -31
package/CLAUDE.md ADDED
@@ -0,0 +1,302 @@
1
+ # CLAUDE.md — @digital-alchemy/core
2
+
3
+ ## 1. What core is
4
+
5
+ `@digital-alchemy/core` is a zero-magic dependency-injection framework for TypeScript. There is no reflection, no decorators, no class hierarchy. Every service is a plain function that receives its dependencies through a single destructured parameter — `TServiceParams`.
6
+
7
+ ```typescript
8
+ export function MyService({ logger, lifecycle, config }: TServiceParams) {
9
+ lifecycle.onReady(() => {
10
+ logger.info("ready");
11
+ });
12
+ return { doThing };
13
+ }
14
+ ```
15
+
16
+ The framework wires up all declared services at bootstrap time, building a dependency graph from the `libraries` and `services` fields of each `CreateApplication` / `CreateLibrary` call. Once wired, every service receives its full `TServiceParams` synchronously (with async factories awaited).
17
+
18
+ This repo is the foundation every other `@digital-alchemy` library depends on. Changes to exports, lifecycle semantics, or `TServiceParams` shape ripple downstream into every consumer. Treat public API surface changes as breaking.
19
+
20
+ ---
21
+
22
+ ## 2. Repo orientation
23
+
24
+ ### `src/services/*.mts` — stateful service factories
25
+
26
+ | File | Owns / responsibility | Read by |
27
+ |---|---|---|
28
+ | `als.service.mts` | `AsyncLocalStorage` wrapper; `enterWith`, `run`, `getStore`, `getLogData` | Logger (ALS log merging), any code needing per-request context |
29
+ | `configuration.service.mts` | Config state map, loader orchestration, proxy for `config.*`, `setConfig`, `mergeConfig`, `validateConfig`, `onUpdate` | `wiring.service.mts` at bootstrap; every service via `config` injection |
30
+ | `internal.service.mts` | `InternalDefinition` class (boot state, module maps, lifecycle ref); `InternalUtils` (object path get/set/del, `is`, `relativeDate`, `titleCase`); `safeExec` | All services — injected as `internal` |
31
+ | `is.service.mts` | `IsIt` class — type guards and small utilities (`array`, `boolean`, `empty`, `equal`, `function`, `object`, `string`, etc.); exports singleton `is` | Imported directly by `lifecycle.service.mts` and `wiring.service.mts` to break circular deps; everywhere else reached as `internal.utils.is` |
32
+ | `lifecycle.service.mts` | `CreateLifecycle` — per-bootstrap lifecycle event registry; priority-sorted callback execution across all seven stages | `wiring.service.mts` owns the lifecycle instance; services attach via `lifecycle.*` injection |
33
+ | `logger.service.mts` | `Logger` factory — chalk/stdout formatter, `context(ctx)` builder, level filtering, `addTarget`, `updateShouldLog`, ALS integration, `systemLogger` | Every service via `logger` injection; `internal.boilerplate.logger` |
34
+ | `scheduler.service.mts` | `Scheduler` factory — cron, interval, sliding, `setTimeout`, `setInterval`, `sleep`; registers stop callbacks for clean shutdown; returns a builder called with `(context: TContext)` | Every service via `scheduler` injection |
35
+ | `wiring.service.mts` | Bootstrap orchestration — `CreateApplication`, `wireService`, `CreateBoilerplate`, `teardown`, SIGINT/SIGTERM handling; owns `LIB_BOILERPLATE` | Entry point for all apps; do not call `wireService` directly |
36
+ | `index.mts` | Re-exports all service exports | Everything in `src/` imports from `../index.mts` |
37
+
38
+ ### `src/helpers/*.mts` — pure logic, types, and utilities
39
+
40
+ **What distinguishes helpers from services:** helpers are side-effect-free modules containing types, utility functions, constants, and small classes. They do not receive `TServiceParams`. Services are factories that receive DI params and return an API object.
41
+
42
+ | File | Owns |
43
+ |---|---|
44
+ | `async.mts` | `each`, `eachSeries`, `eachLimit` — async iteration helpers that replace inconsistent `async` library behavior |
45
+ | `config-environment-loader.mts` | `ConfigLoaderEnvironment` — reads env vars and CLI switches; search order is `MODULE__KEY` (double-underscore) → `MODULE_KEY` (single-underscore) → `KEY` |
46
+ | `config-file-loader.mts` | `configLoaderFile`, `configFilePaths`, `loadConfigFromFile`, `withExtensions` — file-based config loading (JSON, YAML, INI, auto-detect) |
47
+ | `config.mts` | All config types (`AnyConfig`, `StringConfig`, `BooleanConfig`, etc.), `ConfigLoaderParams`, `ConfigLoaderReturn`, `findKey`, `iSearchKey`, `loadDotenv`, `parseConfig`, `KnownConfigs` |
48
+ | `context.mts` | `TContext` branded string type; `IContextBrand` |
49
+ | `cron.mts` | `CronExpression` enum, `TOffset`, `SchedulerCronOptions`, `SchedulerIntervalOptions`, `SchedulerSlidingOptions`, `DigitalAlchemyScheduler`, `SchedulerBuilder` |
50
+ | `errors.mts` | `BootstrapException` (wiring-time errors), `InternalError` (runtime errors) — both carry `context`, `cause`, `timestamp` |
51
+ | `events.mts` | Global error event name constants (`DIGITAL_ALCHEMY_NODE_GLOBAL_ERROR`, etc.) |
52
+ | `extend.mts` | `deepExtend`, `deepCloneArray`, `cloneSpecificValue` — deep merge utilities |
53
+ | `index.mts` | Re-exports all helper exports; new public helpers thread through here |
54
+ | `lifecycle.mts` | `TLifecycleBase`, `TLifeCycleRegister`, `LIFECYCLE_STAGES` array, `LifecycleStages` type, `LifecycleCallback` |
55
+ | `logger.mts` | `ILogger`, `TLoggerFunction`, `DigitalAlchemyLogger`, `GetLogger`, `METHOD_COLORS`, `fatalLog`, `EVENT_UPDATE_LOG_LEVELS` |
56
+ | `module.mts` | `createModule`, `DigitalAlchemyModule`, `ModuleExtension` — chainable module builder that can export to application, library, or test runner |
57
+ | `service-runner.mts` | `ServiceRunner` — minimal one-service bootstrap helper for scripts |
58
+ | `utilities.mts` | Numeric constants (`START`, `NONE`, `EMPTY`, `FIRST`, `ARRAY_OFFSET`, `SINGLE`, `UP`, `DOWN`, `MINUTE`, `HOUR`, `DAY`, `SECOND`, `YEAR`, etc.); `sleep`, `debounce`, `toOffsetMs`, `toOffsetDuration`, `SleepReturn`, `TBlackHole` |
59
+ | `wiring.mts` | `TServiceParams`, `ServiceFunction`, `ServiceMap`, `ApplicationDefinition`, `LibraryDefinition`, `BootstrapOptions`, `TInjectedConfig`, `LoadedModules`, `buildSortOrder`, `CreateLibrary`, `wireOrder`, `COERCE_CONTEXT`, `WIRE_PROJECT` |
60
+
61
+ ### `src/testing/*.mts` — test infrastructure (not test cases)
62
+
63
+ | File | Owns |
64
+ |---|---|
65
+ | `mock-logger.mts` | `createMockLogger()` — returns a no-op `ILogger`; import in specs to suppress output |
66
+ | `test-module.mts` | `TestRunner` factory + `iTestRunner` interface — boots a real DI graph scoped to a test; chainable API: `.configure`, `.setOptions`, `.run`, `.serviceParams`, `.setup`, `.appendLibrary`, `.appendService`, `.replaceLibrary`, `.teardown` |
67
+ | `index.mts` | Re-exports `mock-logger` and `test-module` |
68
+
69
+ ### `testing/` (sibling of `src/`, not inside it)
70
+
71
+ All `.spec.mts` files live here. Each spec file maps to a service or helper:
72
+ `als.spec.mts`, `configuration.spec.mts`, `internal.spec.mts`, `is.spec.mts`, `logger.spec.mts`, `scheduler.spec.mts`, `testing.spec.mts`, `utilities.spec.mts`, `wiring.spec.mts`. A `setup.mts` file and `pipelines/` subdirectory handle vitest global setup.
73
+
74
+ ---
75
+
76
+ ## 3. The TServiceParams pattern in core specifically
77
+
78
+ ### Reaching `is`
79
+
80
+ **Inside core services and helpers**, use `internal.utils.is` — do not `import { is }` from the index:
81
+
82
+ ```typescript
83
+ // correct inside a service factory
84
+ export function MyService({ internal }: TServiceParams) {
85
+ const { is } = internal.utils;
86
+ if (is.empty(value)) { ... }
87
+ }
88
+ ```
89
+
90
+ **The one exception:** `is.service.mts` is imported directly in `lifecycle.service.mts` and `wiring.service.mts` to break a circular dependency that would form if they went through `../index.mts`. This is intentional — leave it.
91
+
92
+ ### Dual-arity logger with `{ name: fnRef }` tagging
93
+
94
+ Every function entry logs with its own function reference as the `name` field. This appears in the rendered output as the function name, enabling precise call-site identification without string literals.
95
+
96
+ ```typescript
97
+ function loadThing(id: string) {
98
+ logger.trace({ name: loadThing, id }, "loading");
99
+ // ...
100
+ logger.debug({ name: loadThing }, "loaded");
101
+ }
102
+ ```
103
+
104
+ Pass the function reference directly (not `loadThing.name`). The logger extracts `.name` from the object or function automatically.
105
+
106
+ ### Lifecycle hook order
107
+
108
+ Hooks are attached in service constructors but executed by the wiring engine in strict order:
109
+
110
+ ```
111
+ onPreInit → onPostConfig → onBootstrap → onReady
112
+ → onPreShutdown → onShutdownStart → onShutdownComplete
113
+ ```
114
+
115
+ - `onPreInit`: safe for attaching state; config is not yet loaded — do not read `config.*`
116
+ - `onPostConfig`: config values are available from this point forward
117
+ - `onBootstrap`: all modules are wired; safe to call other services
118
+ - `onReady`: app is fully running; schedulers start here
119
+ - `onPreShutdown` / `onShutdownStart` / `onShutdownComplete`: teardown in order
120
+
121
+ Each hook accepts an optional `priority` number. Positive priorities run first (high → low), un-prioritized callbacks run in parallel, negative priorities run last.
122
+
123
+ ### No `metrics` injection
124
+
125
+ `TServiceParams` in this repo does not include `metrics`. Do not write `metrics.perf()`, `metrics.histogram(...)`, or any metrics-instrumentation patterns. Those patterns exist in sibling repos (e.g., vault-ts) that build on top of core. Core itself has no metrics service.
126
+
127
+ ### `context: TContext` for builder-style services
128
+
129
+ `scheduler` is a builder: it returns a function that accepts `context: TContext` and returns the actual scheduler API. This is the pattern for any service that needs to bind per-caller context at use time:
130
+
131
+ ```typescript
132
+ // wiring.service.mts — how scheduler is injected
133
+ scheduler: boilerplate?.scheduler?.(context),
134
+ ```
135
+
136
+ Services that need to pass context down into callbacks capture the context injected into their own factory:
137
+
138
+ ```typescript
139
+ export function MyService({ context, scheduler }: TServiceParams) {
140
+ scheduler.cron({ exec: doWork, schedule: CronExpression.EVERY_MINUTE });
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 4. Style rules
147
+
148
+ ### TSDoc
149
+
150
+ - Every exported factory function gets a TSDoc comment with at minimum a one-line summary.
151
+ - Every method on the returned object gets a one-line summary.
152
+ - Use `@remarks` for non-obvious behavior (e.g., "only valid after `onPostConfig`").
153
+ - Use `@throws` when the function throws a documented exception.
154
+ - `@internal` on symbols not intended for downstream consumers.
155
+
156
+ ### Inline comments
157
+
158
+ Write `// why`, never `// what`. The code describes what. The comment explains the reason:
159
+
160
+ ```typescript
161
+ // only read config after PostConfig fires; value is undefined before that
162
+ lifecycle.onPostConfig(() => {
163
+ CURRENT_LOG_LEVEL = config.boilerplate.LOG_LEVEL;
164
+ });
165
+ ```
166
+
167
+ ### Logging conventions
168
+
169
+ ```typescript
170
+ // entry — always trace with name + relevant inputs
171
+ logger.trace({ name: fnRef, ...inputs }, "brief description");
172
+
173
+ // decision points — trace
174
+ logger.trace({ name: fnRef, chosen }, "selected path");
175
+
176
+ // successful completion — debug
177
+ logger.debug({ name: fnRef }, "operation complete");
178
+
179
+ // expected-but-notable — info
180
+ // unexpected non-fatal — warn
181
+ // errors — error or fatal
182
+ ```
183
+
184
+ ### Error types
185
+
186
+ - `BootstrapException(context, "SCREAMING_SNAKE_CODE", "human message")` — for wiring failures, misconfiguration, or anything that should prevent boot.
187
+ - `InternalError(context, "CODE", "message")` — for runtime logic errors after boot.
188
+ - No bare `new Error(...)` in service or helper code.
189
+
190
+ ### Hard prohibitions
191
+
192
+ - No `console.*` — use `logger.*` or `fatalLog` for last-resort stderr writes.
193
+ - No `process.stdout` writes.
194
+ - No `process.env.*` access outside of `config-environment-loader.mts` and `wiring.service.mts` (where `IS_TEST` / `NODE_ENV` defaults are set).
195
+ - No module-level side effects beyond constant declarations and `dayjs.extend(...)`.
196
+ - No `.catch()` — use `internal.safeExec` for wrapped async execution, or handle errors explicitly.
197
+ - No classic `for` / `for...of` loops — use `each`, `eachSeries`, `eachLimit`, `.forEach`, `.map`, `.filter`, `.some`, `.find`.
198
+ - No `ts-ignore` on new code; `ts-expect-error` is acceptable only with a comment explaining why.
199
+
200
+ ### Cast restrictions
201
+
202
+ Follow the existing eslint config. In particular: `@typescript-eslint/no-magic-numbers` is enforced — declare named constants for non-obvious numeric literals.
203
+
204
+ ---
205
+
206
+ ## 5. Validation
207
+
208
+ All commands run from the repo root (`/home/zoe/Repos/DigitalAlchemyTS/core`).
209
+
210
+ ### `yarn lint`
211
+
212
+ Runs eslint over `src/` and `testing/`. Must pass with zero errors and zero warnings. A clean run exits 0 with no output.
213
+
214
+ ```bash
215
+ yarn lint
216
+ ```
217
+
218
+ ### `yarn test`
219
+
220
+ Runs vitest. All specs in `testing/` must pass. A clean run shows all tests green with no skipped specs (unless the test is explicitly marked `.skip` for a documented reason).
221
+
222
+ ```bash
223
+ yarn test
224
+ ```
225
+
226
+ ### Type check the publish surface
227
+
228
+ ```bash
229
+ tsc -p tsconfig.lib.json --noEmit
230
+ ```
231
+
232
+ This checks only the files that end up in `dist/`. Errors here are publish-blocking.
233
+
234
+ ### `yarn build`
235
+
236
+ Produces `dist/` cleanly. Use to verify the publish output compiles.
237
+
238
+ ```bash
239
+ yarn build
240
+ ```
241
+
242
+ ### Do not touch
243
+
244
+ - `coverage/` — generated artifact, gitignored
245
+ - `node_modules/` — managed by yarn
246
+ - `yarn.lock` — only update deliberately with `yarn add` / `yarn remove`
247
+ - `dist/` — gitignored; never commit it
248
+
249
+ ---
250
+
251
+ ## 6. Footguns specific to core
252
+
253
+ ### `wiring.mts` and `wiring.service.mts` — the engine was deliberately split
254
+
255
+ `helpers/wiring.mts` contains all the types and pure functions (`CreateLibrary`, `buildSortOrder`, `TServiceParams`, etc.). `services/wiring.service.mts` contains the runtime bootstrap logic (`CreateApplication`, `bootstrap`, `wireService`, `teardown`).
256
+
257
+ This split exists to break a circular reference that would form if types and the bootstrap runtime lived in the same file. **Do not merge them.** Read both before touching either. Changes to `TServiceParams` shape in `helpers/wiring.mts` affect every downstream library.
258
+
259
+ ### `index.mts` is the re-export hub
260
+
261
+ `src/index.mts` re-exports everything. New public symbols thread through either `src/helpers/index.mts` or `src/services/index.mts` (whichever is appropriate), not directly into `src/index.mts`.
262
+
263
+ ### Services import from `../index.mts`, not from siblings
264
+
265
+ Services import from `../index.mts` rather than directly from sibling files:
266
+
267
+ ```typescript
268
+ // correct
269
+ import { deepExtend, eachSeries } from "../index.mts";
270
+
271
+ // wrong — breaks the circular-ref mitigation
272
+ import { deepExtend } from "./extend.mts";
273
+ ```
274
+
275
+ The only exceptions are the deliberate direct imports in `lifecycle.service.mts` and `wiring.service.mts` of `is.service.mts`.
276
+
277
+ ### `lifecycle.service.mts` imports `is` directly
278
+
279
+ `lifecycle.service.mts` and `wiring.service.mts` both `import { is } from "./is.service.mts"` instead of going through `../index.mts`. This breaks the circular dep that would form at module init time. Leave this pattern intact.
280
+
281
+ ### `TestRunner` boots a real DI graph
282
+
283
+ `TestRunner` in `src/testing/test-module.mts` bootstraps a full application with real lifecycle execution. It is not a mock framework. This means:
284
+
285
+ - Lifecycle hooks fire in real order.
286
+ - Assertions inside `lifecycle.onPostConfig(...)` only see post-config values.
287
+ - Services passed to `.appendLibrary` / `.appendService` execute normally.
288
+ - Always call `.teardown()` in `afterEach` — open handles will keep vitest hanging.
289
+
290
+ Mocking depth matters: spying on a method only replaces that method, not the service it belongs to. When in doubt, use `.replaceLibrary` or inject a stub via `.appendService`.
291
+
292
+ ### `configSources` controls loader activation
293
+
294
+ By default, `TestRunner` does not load env vars or config files (this is the safe default for tests). To test env-var resolution, pass `loadConfigs: true` or `setOptions({ configSources: { env: true } })`. To assert that env loading is disabled, pass `configSources: { env: false }`.
295
+
296
+ ---
297
+
298
+ ## 7. Where to find more
299
+
300
+ Long-form learning material — architecture overviews, declaration merging docs, advanced recipes — exists in a sibling documentation repository. Refer to that repo abstractly; its path and URL are out of scope here.
301
+
302
+ This `CLAUDE.md` is the canonical working reference for the `core` repo itself. When this file and external docs disagree about what to do *in this repo*, this file wins.
package/README.md CHANGED
@@ -17,11 +17,27 @@ yarn add @digital-alchemy/core
17
17
 
18
18
  ## Introduction
19
19
 
20
- The Digital Alchemy core utilities are a set of dependency-light tools for building backend applications with **TypeScript**. It targets the latest **ESModule** syntax and language standards, and it's compatible with Bun, Deno, and modern versions of NodeJS.
20
+ `@digital-alchemy/core` is a dependency-injection framework for TypeScript no decorators, no reflection, no class hierarchy. Services are plain functions that receive their dependencies through a single typed parameter. The framework wires everything at boot time, with full type safety across your entire service graph.
21
21
 
22
- Modules leverage advanced TypeScript features to easily combine services and configurations into type-safe applications. This makes it friendly to a variety of use cases, from complex functional programming logic to usage as a smaller utility in an existing codebase.
22
+ Targets the latest ESModule syntax and runs on Bun, Deno, and modern Node.
23
+
24
+ ## At a glance
25
+
26
+ ```typescript
27
+ import { CreateApplication, TServiceParams } from "@digital-alchemy/core";
28
+
29
+ function HelloService({ logger, lifecycle }: TServiceParams) {
30
+ lifecycle.onReady(() => logger.info("hello world"));
31
+ }
32
+
33
+ const app = CreateApplication({
34
+ name: "hello",
35
+ services: { hello: HelloService },
36
+ });
37
+
38
+ await app.bootstrap();
39
+ ```
23
40
 
24
- The framework adds minimal overhead to boot times, making it well-suited for a wide range of applications, such as web servers, serverless functions, automation tools, and long-running background scripts.
25
41
  ## What it does
26
42
 
27
43
  - **Service wiring** - Automatic dependency injection with full type safety
@@ -1,3 +1,40 @@
1
+ /**
2
+ * Async iteration helpers — drop-in replacements for the `async` library with
3
+ * more predictable behavior.
4
+ *
5
+ * @remarks
6
+ * Provides `each`, `eachSeries`, and `eachLimit` functions that mirror common
7
+ * async iteration patterns but with consistent semantics. All functions accept
8
+ * either an array or a Set as input and execute a callback on each item,
9
+ * returning a promise that resolves when all iterations complete.
10
+ */
11
+ /**
12
+ * Execute an async callback in parallel on each item in a collection.
13
+ *
14
+ * @remarks
15
+ * Invokes all callbacks concurrently using `Promise.all`. Accepts either an
16
+ * array or a Set; Sets are converted to arrays before processing. If `callback`
17
+ * throws or rejects, the entire operation fails with that error.
18
+ */
1
19
  export declare function each<T = unknown>(item: T[] | Set<T>, callback: (item: T) => Promise<void | unknown>): Promise<void>;
20
+ /**
21
+ * Execute an async callback serially on each item in a collection.
22
+ *
23
+ * @remarks
24
+ * Invokes callbacks one after the other, waiting for each to complete before
25
+ * starting the next. Accepts either an array or a Set; Sets are converted to
26
+ * arrays. Throws if the input is not a Set or array after conversion.
27
+ *
28
+ * @throws {TypeError} when the input is neither an array nor a Set.
29
+ */
2
30
  export declare function eachSeries<T = void>(item: T[] | Set<T>, callback: (item: T) => Promise<void | unknown>): Promise<void>;
31
+ /**
32
+ * Execute an async callback on each item in an array while limiting concurrency.
33
+ *
34
+ * @remarks
35
+ * Respects a maximum number of concurrent callbacks by tracking active promises
36
+ * and awaiting one to resolve before starting a new one. Processes the first
37
+ * `limit` items immediately, then maintains the limit as remaining items are
38
+ * queued. Resolves only when all callbacks complete.
39
+ */
3
40
  export declare function eachLimit<T = unknown>(items: T[], limit: number, callback: (item: T) => Promise<void | unknown>): Promise<void>;
@@ -1,21 +1,46 @@
1
+ /**
2
+ * Async iteration helpers — drop-in replacements for the `async` library with
3
+ * more predictable behavior.
4
+ *
5
+ * @remarks
6
+ * Provides `each`, `eachSeries`, and `eachLimit` functions that mirror common
7
+ * async iteration patterns but with consistent semantics. All functions accept
8
+ * either an array or a Set as input and execute a callback on each item,
9
+ * returning a promise that resolves when all iterations complete.
10
+ */
1
11
  import { is } from "../index.mjs";
2
12
  import { ARRAY_OFFSET, SINGLE, START } from "./utilities.mjs";
3
- // ? Functions written to be similar to the offerings from the async library
4
- // That library gave me oddly inconsistent results,
5
- // so these exist to replace those doing exactly what I expect
6
- //
7
- // #MARK: each
13
+ /**
14
+ * Execute an async callback in parallel on each item in a collection.
15
+ *
16
+ * @remarks
17
+ * Invokes all callbacks concurrently using `Promise.all`. Accepts either an
18
+ * array or a Set; Sets are converted to arrays before processing. If `callback`
19
+ * throws or rejects, the entire operation fails with that error.
20
+ */
8
21
  export async function each(item, callback) {
22
+ // convert Set to array for uniform handling
9
23
  if (item instanceof Set) {
10
24
  item = [...item.values()];
11
25
  }
12
26
  await Promise.all(item.map(async (i) => await callback(i)));
13
27
  }
14
- // #MARK: eachSeries
28
+ /**
29
+ * Execute an async callback serially on each item in a collection.
30
+ *
31
+ * @remarks
32
+ * Invokes callbacks one after the other, waiting for each to complete before
33
+ * starting the next. Accepts either an array or a Set; Sets are converted to
34
+ * arrays. Throws if the input is not a Set or array after conversion.
35
+ *
36
+ * @throws {TypeError} when the input is neither an array nor a Set.
37
+ */
15
38
  export async function eachSeries(item, callback) {
39
+ // convert Set to array for uniform handling
16
40
  if (item instanceof Set) {
17
41
  item = [...item.values()];
18
42
  }
43
+ // ensure we have an array; failing fast helps catch misuse early
19
44
  if (!is.array(item)) {
20
45
  throw new TypeError(`not provided an array`);
21
46
  }
@@ -23,29 +48,39 @@ export async function eachSeries(item, callback) {
23
48
  await callback(item[i]);
24
49
  }
25
50
  }
26
- // #MARK: eachLimit
51
+ /**
52
+ * Execute an async callback on each item in an array while limiting concurrency.
53
+ *
54
+ * @remarks
55
+ * Respects a maximum number of concurrent callbacks by tracking active promises
56
+ * and awaiting one to resolve before starting a new one. Processes the first
57
+ * `limit` items immediately, then maintains the limit as remaining items are
58
+ * queued. Resolves only when all callbacks complete.
59
+ */
27
60
  export async function eachLimit(items, limit, callback) {
28
- // Track active promises to ensure we don't exceed the limit
61
+ // track promises to enforce the concurrency limit
29
62
  const activePromises = new Set();
30
- // A helper function to add a new task
63
+ // queue a new callback and manage the promise lifecycle
31
64
  async function addTask(item) {
32
65
  const promise = callback(item).then(() => {
33
- activePromises.delete(promise); // Remove the promise from the set once it's resolved
66
+ // clean up completed promise so we can detect when to start new ones
67
+ activePromises.delete(promise);
34
68
  });
35
69
  activePromises.add(promise);
70
+ // if at or over limit, block until at least one promise resolves
36
71
  if (activePromises.size >= limit) {
37
- await Promise.race(activePromises); // Wait for one of the active promises to resolve
72
+ await Promise.race(activePromises);
38
73
  }
39
74
  }
40
- // Add initial tasks up to the limit
75
+ // seed the concurrency pool with the first `limit` items
41
76
  const initialTasks = items.slice(SINGLE, limit).map(item => addTask(item));
42
- // Wait for the initial set of tasks to start processing
77
+ // wait for all initial tasks to be queued (not necessarily complete)
43
78
  await Promise.all(initialTasks);
44
- // Process the remaining items, ensuring the limit is respected
79
+ // process remaining items while respecting the limit
45
80
  for (let i = limit - ARRAY_OFFSET; i < items.length; i++) {
46
81
  await addTask(items[i]);
47
82
  }
48
- // Wait for all remaining tasks to complete
83
+ // wait for all in-flight operations to finish
49
84
  await Promise.all(activePromises);
50
85
  }
51
86
  //# sourceMappingURL=async.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"async.mjs","sourceRoot":"","sources":["../../src/helpers/async.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE9D,4EAA4E;AAC5E,mDAAmD;AACnD,kEAAkE;AAClE,EAAE;AAEF,cAAc;AACd,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,IAAkB,EAClB,QAA8C;IAE9C,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC,CAAC,EAAC,EAAE,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,oBAAoB;AACpB,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAkB,EAClB,QAA8C;IAE9C,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,mBAAmB;AACnB,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAU,EACV,KAAa,EACb,QAA8C;IAE9C,4DAA4D;IAC5D,MAAM,cAAc,GAAiC,IAAI,GAAG,EAAE,CAAC;IAE/D,sCAAsC;IACtC,KAAK,UAAU,OAAO,CAAC,IAAO;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACvC,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,qDAAqD;QACvF,CAAC,CAAC,CAAC;QACH,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5B,IAAI,cAAc,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;YACjC,MAAM,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,iDAAiD;QACvF,CAAC;IACH,CAAC;IAED,oCAAoC;IACpC,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAE3E,wDAAwD;IACxD,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAEhC,+DAA+D;IAC/D,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,2CAA2C;IAC3C,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"async.mjs","sourceRoot":"","sources":["../../src/helpers/async.mts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,IAAkB,EAClB,QAA8C;IAE9C,4CAA4C;IAC5C,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC,CAAC,EAAC,EAAE,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAkB,EAClB,QAA8C;IAE9C,4CAA4C;IAC5C,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAU,EACV,KAAa,EACb,QAA8C;IAE9C,kDAAkD;IAClD,MAAM,cAAc,GAAiC,IAAI,GAAG,EAAE,CAAC;IAE/D,wDAAwD;IACxD,KAAK,UAAU,OAAO,CAAC,IAAO;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACvC,qEAAqE;YACrE,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QACH,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5B,iEAAiE;QACjE,IAAI,cAAc,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;YACjC,MAAM,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,yDAAyD;IACzD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAE3E,qEAAqE;IACrE,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAEhC,qDAAqD;IACrD,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,8CAA8C;IAC9C,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACpC,CAAC"}
@@ -1,5 +1,44 @@
1
+ /**
2
+ * Environment variable and CLI switch config loader.
3
+ *
4
+ * @remarks
5
+ * Reads config from Node.js env vars and command-line arguments. For each config
6
+ * key, searches using a three-tier precedence: CLI switches (highest), then env
7
+ * vars (middle), then defaults (lowest). Both argv and env sources support a
8
+ * double-underscore variant (`MODULE__KEY`) in addition to single-underscore,
9
+ * enabling env vars with embedded dots/slashes that would be shell-escaped.
10
+ * Timing information is captured and returned for observability.
11
+ */
1
12
  import type { ConfigLoaderParams, ConfigLoaderReturn, ModuleConfiguration } from "./config.mts";
2
13
  import type { ServiceMap } from "./wiring.mts";
14
+ /**
15
+ * Load configuration from environment variables and CLI switches.
16
+ *
17
+ * @remarks
18
+ * Merges CLI arguments and environment variables into the config tree, respecting
19
+ * the configured sources (argv, env) and individual config key source restrictions.
20
+ * Searches for each key in three forms (in order):
21
+ * 1. `MODULE__KEY` (double underscore) — preferred, allows shell-escaped env vars
22
+ * 2. `MODULE_KEY` (single underscore) — classic format
23
+ * 3. `KEY` (bare key) — fallback for globals
24
+ *
25
+ * CLI arguments are checked first (highest precedence); env vars are checked second.
26
+ * Both are optional and controlled by `internal.boot.options.configSources`.
27
+ *
28
+ * Timing data is collected for argv and env separately and returned if the
29
+ * optional `timings` object is provided.
30
+ *
31
+ * @example
32
+ * ```
33
+ * // Search order for app.NODE_ENV config key:
34
+ * // 1. --app__NODE_ENV cli switch
35
+ * // 2. APP__NODE_ENV env var
36
+ * // 3. --app_NODE_ENV cli switch
37
+ * // 4. APP_NODE_ENV env var
38
+ * // 5. --NODE_ENV cli switch
39
+ * // 6. NODE_ENV env var
40
+ * ```
41
+ */
3
42
  export declare function ConfigLoaderEnvironment<S extends ServiceMap = ServiceMap, C extends ModuleConfiguration = ModuleConfiguration>({ configs, internal, logger, timings, }: ConfigLoaderParams<S, C> & {
4
43
  timings?: Record<string, string>;
5
44
  }): ConfigLoaderReturn;
@@ -1,7 +1,46 @@
1
+ /**
2
+ * Environment variable and CLI switch config loader.
3
+ *
4
+ * @remarks
5
+ * Reads config from Node.js env vars and command-line arguments. For each config
6
+ * key, searches using a three-tier precedence: CLI switches (highest), then env
7
+ * vars (middle), then defaults (lowest). Both argv and env sources support a
8
+ * double-underscore variant (`MODULE__KEY`) in addition to single-underscore,
9
+ * enabling env vars with embedded dots/slashes that would be shell-escaped.
10
+ * Timing information is captured and returned for observability.
11
+ */
1
12
  import { env } from "node:process";
2
13
  import minimist from "minimist";
3
14
  import { is, NONE } from "../index.mjs";
4
15
  import { findKey, iSearchKey, loadDotenv, parseConfig } from "./config.mjs";
16
+ /**
17
+ * Load configuration from environment variables and CLI switches.
18
+ *
19
+ * @remarks
20
+ * Merges CLI arguments and environment variables into the config tree, respecting
21
+ * the configured sources (argv, env) and individual config key source restrictions.
22
+ * Searches for each key in three forms (in order):
23
+ * 1. `MODULE__KEY` (double underscore) — preferred, allows shell-escaped env vars
24
+ * 2. `MODULE_KEY` (single underscore) — classic format
25
+ * 3. `KEY` (bare key) — fallback for globals
26
+ *
27
+ * CLI arguments are checked first (highest precedence); env vars are checked second.
28
+ * Both are optional and controlled by `internal.boot.options.configSources`.
29
+ *
30
+ * Timing data is collected for argv and env separately and returned if the
31
+ * optional `timings` object is provided.
32
+ *
33
+ * @example
34
+ * ```
35
+ * // Search order for app.NODE_ENV config key:
36
+ * // 1. --app__NODE_ENV cli switch
37
+ * // 2. APP__NODE_ENV env var
38
+ * // 3. --app_NODE_ENV cli switch
39
+ * // 4. APP_NODE_ENV env var
40
+ * // 5. --NODE_ENV cli switch
41
+ * // 6. NODE_ENV env var
42
+ * ```
43
+ */
5
44
  export async function ConfigLoaderEnvironment({ configs, internal, logger, timings, }) {
6
45
  const DECIMALS = 2;
7
46
  const CLI_SWITCHES = minimist(process.argv);
@@ -11,27 +50,26 @@ export async function ConfigLoaderEnvironment({ configs, internal, logger, timin
11
50
  const canArgv = internal.boot.options?.configSources?.argv ?? true;
12
51
  const shouldArgv = (source) => canArgv && (!is.array(source) || source.includes("argv"));
13
52
  const shouldEnv = (source) => canEnvironment && (!is.array(source) || source.includes("env"));
14
- // * merge dotenv into local vars
15
- // accounts for `--env-file` switches, and whatever is passed in via bootstrap
53
+ // merge dotenv files and env-file switches into process.env
16
54
  loadDotenv(internal, CLI_SWITCHES, logger);
17
55
  const environmentKeys = Object.keys(env);
18
- // Track timing for argv and env separately
56
+ // track timing for argv and env separately
19
57
  let argvTime = NONE;
20
58
  let envTime = NONE;
21
- // * go through all module
59
+ // iterate through all module configurations
22
60
  configs.forEach((configuration, project) => {
23
61
  const cleanedProject = project.replaceAll("-", "_");
24
- // * run through each config for module
62
+ // iterate through each config key within the module
25
63
  Object.keys(configuration).forEach(key => {
26
64
  const { source } = configs.get(project)[key];
27
- // > things to search for
28
- // - MODULE_NAME_CONFIG_KEY (module + key, ex: app_NODE_ENV)
29
- // - CONFIG_KEY (only key, ex: NODE_ENV)
65
+ // search keys in order: double-underscore (preferred), single-underscore, bare key
66
+ // double-underscore allows env var names with embedded special chars (dots, slashes)
30
67
  const noAppPath = `${cleanedProject}_${key}`;
31
- const search = [noAppPath, key];
68
+ const noAppPathDouble = `${cleanedProject}__${key}`;
69
+ const search = [noAppPathDouble, noAppPath, key];
32
70
  const configPath = `${project}.${key}`;
33
71
  if (canArgv) {
34
- // * (preferred) Find an applicable cli switch
72
+ // CLI switches take precedence; find a matching flag in the argv
35
73
  const argvStart = performance.now();
36
74
  const flag = findKey(search, switchKeys);
37
75
  if (flag && shouldArgv(source)) {
@@ -47,12 +85,13 @@ export async function ConfigLoaderEnvironment({ configs, internal, logger, timin
47
85
  }
48
86
  argvTime += performance.now() - argvStart;
49
87
  }
50
- // * (fallback) Find an environment variable
88
+ // fallback to environment variable if no CLI switch was found
51
89
  if (canEnvironment) {
52
90
  const envStart = performance.now();
53
91
  const environment = findKey(search, environmentKeys);
54
92
  if (!is.empty(environment) && shouldEnv(source)) {
55
93
  const environmentName = iSearchKey(environment, environmentKeys);
94
+ // only set if the env var is defined and non-empty
56
95
  if (!is.string(env[environmentName]) || !is.empty(env[environmentName])) {
57
96
  internal.utils.object.set(out, configPath, parseConfig(configuration[key], env[environmentName]));
58
97
  }
@@ -69,6 +108,7 @@ export async function ConfigLoaderEnvironment({ configs, internal, logger, timin
69
108
  }
70
109
  });
71
110
  });
111
+ // record timing if provided
72
112
  if (timings) {
73
113
  if (argvTime) {
74
114
  timings.argv = `${argvTime.toFixed(DECIMALS)}ms`;