@asaidimu/utils-artifacts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,799 @@
1
+ # @asaidimu/utils-artifacts
2
+
3
+ A powerful, reactive dependency injection container for managing application artifacts and their lifecycles. It provides a robust framework for building modular, maintainable, and highly responsive applications by decoupling components and managing their state-driven dependencies.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-artifacts.svg?style=flat-square)](https://www.npmjs.com/package/@asaidimu/utils-artifacts)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
7
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/asaidimu/utils/ci.yml?branch=main&style=flat-square)](https://github.com/asaidimu/utils/actions?query=workflow%3ACI)
8
+
9
+ ## 🚀 Quick Links
10
+
11
+ * [Overview & Features](#-overview--features)
12
+ * [Installation & Setup](#-installation--setup)
13
+ * [Usage Documentation](#-usage-documentation)
14
+ * [Basic Artifacts (Singleton & Transient)](#basic-artifacts-singleton--transient)
15
+ * [Reactive State Dependencies](#reactive-state-dependencies)
16
+ * [Artifact-to-Artifact Dependencies](#artifact-to-artifact-dependencies)
17
+ * [Lifecycle Hooks: `onCleanup` & `onDispose`](#lifecycle-hooks-oncleanup--ondispose)
18
+ * [Watching Artifacts for Changes](#watching-artifacts-for-changes)
19
+ * [Yielding Intermediate Values](#yielding-intermediate-values)
20
+ * [Hierarchical Containers](#hierarchical-containers)
21
+ * [Debugging Information](#debugging-information)
22
+ * [Project Architecture](#-project-architecture)
23
+ * [Development & Contributing](#-development--contributing)
24
+ * [Additional Information](#-additional-information)
25
+
26
+ ---
27
+
28
+ ⚠️ **Beta Warning**
29
+ This package is currently in **beta**. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!
30
+
31
+ ## 📚 Overview & Features
32
+
33
+ `@asaidimu/utils-artifacts` is a sophisticated dependency injection (DI) container designed for modern TypeScript applications. It allows you to register "artifacts" – which can be any JavaScript object, function, or value – and manage their creation, lifecycle, and dependencies. What sets it apart is its reactive nature: artifacts can declare dependencies on both other artifacts and specific slices of a global application state (managed by an external `DataStore` like `@asaidimu/utils-store`). When these declared dependencies change, the container automatically invalidates and rebuilds the affected artifacts, propagating updates through the dependency graph efficiently, with support for debouncing to prevent excessive rebuilds.
34
+
35
+ This package is ideal for building complex, event-driven systems, front-end applications, or backend services where state changes need to trigger logical recalculations or UI updates in a structured and testable manner. It promotes modularity, testability, and maintainability by centralizing dependency management and automating reactive updates.
36
+
37
+ ### ✨ Key Features
38
+
39
+ * **Reactive Dependency Injection**: Automatically invalidates and rebuilds artifacts when their declared dependencies (other artifacts or global state slices) change.
40
+ * **Singleton & Transient Scopes**: Control whether an artifact is instantiated once and reused (`Singleton`) or created fresh on every request (`Transient`).
41
+ * **Hierarchical Containers**: Create child containers that can inherit and resolve artifacts from their parents, enabling scoped dependency trees.
42
+ * **State-Driven Invalidation with Debouncing**: Integrates with a `DataStore` to track state dependencies, triggering artifact rebuilds only when relevant state paths change, with configurable debouncing.
43
+ * **Fine-grained Dependency Tracking**: Explicitly declare dependencies on other artifacts using `ctx.resolve()` and on state slices using `ctx.select()`.
44
+ * **Robust Error Handling**: Built-in detection and specific error types for circular dependencies, missing artifacts, and illegal operations, distinguishing between `system` (structural) and `external` (runtime) errors.
45
+ * **Lifecycle Hooks**: Register `onCleanup` callbacks for instance-specific resource release and `onDispose` for final resource teardown.
46
+ * **Asynchronous Artifact Resolution**: Supports `async`/`await` in artifact factories, with configurable retries for external failures and timeouts.
47
+ * **`ArtifactWatcher` for Reactive Observation**: Observe changes to an artifact's resolved value over time, subscribing to updates without repeatedly resolving.
48
+ * **Live Updates via `yield()`**: Singleton artifacts can proactively update their value from within their factory, immediately notifying dependents and watchers.
49
+ * **Comprehensive Debugging Info**: `getDebugInfo()` provides a snapshot of the container's state, including artifact statuses, dependencies, and dependents.
50
+
51
+ ---
52
+
53
+ ## 📦 Installation & Setup
54
+
55
+ ### Prerequisites
56
+
57
+ * Node.js (v18 or higher recommended)
58
+ * A package manager like Bun, npm, or Yarn.
59
+ * An implementation of the `DataStore` interface for reactive state management. We highly recommend using [`@asaidimu/utils-store`](https://www.npmjs.com/package/@asaidimu/utils-store).
60
+
61
+ ### Installation Steps
62
+
63
+ Install the package using your preferred package manager:
64
+
65
+ ```bash
66
+ # With Bun
67
+ bun add @asaidimu/utils-artifacts
68
+
69
+ # With npm
70
+ npm install @asaidimu/utils-artifacts
71
+
72
+ # With Yarn
73
+ yarn add @asaidimu/utils-artifacts
74
+ ```
75
+
76
+ ### Configuration
77
+
78
+ To use the `ArtifactContainer`, you need to initialize it with an object that provides `get` and `watch` methods conforming to the `DataStore` interface. This allows the container to read the current application state and subscribe to state changes for reactive invalidation.
79
+
80
+ If you're using `@asaidimu/utils-store` as your `DataStore`:
81
+
82
+ ```typescript
83
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
84
+ import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming this provides DataStore interface
85
+
86
+ interface AppState {
87
+ config: { theme: string; debugMode: boolean };
88
+ user: { id: string; name: string };
89
+ data: number[];
90
+ }
91
+
92
+ const initialAppState: AppState = {
93
+ config: { theme: 'dark', debugMode: true },
94
+ user: { id: 'user-123', name: 'Augustine' },
95
+ data: [1, 2, 3]
96
+ };
97
+
98
+ const store = new ReactiveDataStore<AppState>(initialAppState);
99
+
100
+ // Initialize the ArtifactContainer
101
+ const container = new ArtifactContainer<Record<string, any>, AppState>(store);
102
+
103
+ // Now you can start registering artifacts
104
+ ```
105
+
106
+ ### Verification
107
+
108
+ You can verify the installation by trying to import and instantiate the `ArtifactContainer`:
109
+
110
+ ```typescript
111
+ import { ArtifactContainer } from '@asaidimu/utils-artifacts';
112
+ // Assuming a mock or real DataStore is available
113
+ const mockStore = {
114
+ get: () => ({}),
115
+ watch: () => () => {},
116
+ };
117
+ const container = new ArtifactContainer(mockStore);
118
+ console.log('ArtifactContainer initialized successfully!');
119
+ // If no errors, the installation is successful.
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 💡 Usage Documentation
125
+
126
+ ### Basic Artifacts (Singleton & Transient)
127
+
128
+ Artifacts can be registered with different scopes, controlling their lifecycle.
129
+
130
+ #### Singleton Scope
131
+
132
+ A singleton artifact is created only once. Subsequent `resolve` calls return the same instance.
133
+
134
+ ```typescript
135
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
136
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
137
+
138
+ const store = new ReactiveDataStore({});
139
+ const container = new ArtifactContainer(store);
140
+
141
+ let buildCount = 0;
142
+
143
+ container.register({
144
+ key: 'mySingletonService',
145
+ factory: () => {
146
+ buildCount++;
147
+ console.log('Building mySingletonService...');
148
+ return {
149
+ name: 'SingletonService',
150
+ version: '1.0.0'
151
+ };
152
+ },
153
+ scope: ArtifactScope.Singleton,
154
+ lazy: false, // Build immediately on registration
155
+ });
156
+
157
+ async function runSingletonExample() {
158
+ const service1 = await container.resolve('mySingletonService');
159
+ const service2 = await container.resolve('mySingletonService');
160
+
161
+ console.log('Service 1:', service1.instance);
162
+ console.log('Service 2:', service2.instance);
163
+
164
+ console.assert(buildCount === 1, 'Singleton factory should be called once');
165
+ console.assert(service1.instance === service2.instance, 'Both resolutions should return the same instance');
166
+ console.assert(service1.ready, 'Singleton should be ready');
167
+
168
+ // Output:
169
+ // Building mySingletonService...
170
+ // Service 1: { name: 'SingletonService', version: '1.0.0' }
171
+ // Service 2: { name: 'SingletonService', version: '1.0.0' }
172
+ }
173
+
174
+ runSingletonExample();
175
+ ```
176
+
177
+ #### Transient Scope
178
+
179
+ A transient artifact creates a new instance every time it is resolved.
180
+
181
+ ```typescript
182
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
183
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
184
+
185
+ const store = new ReactiveDataStore({});
186
+ const container = new ArtifactContainer(store);
187
+
188
+ container.register({
189
+ key: 'myTransientFactory',
190
+ factory: () => {
191
+ console.log('Building myTransientFactory...');
192
+ return {
193
+ id: Math.random().toString(36).substring(7),
194
+ timestamp: Date.now()
195
+ };
196
+ },
197
+ scope: ArtifactScope.Transient,
198
+ });
199
+
200
+ async function runTransientExample() {
201
+ const instance1 = await container.resolve('myTransientFactory');
202
+ const instance2 = await container.resolve('myTransientFactory');
203
+
204
+ console.log('Instance 1:', instance1.instance);
205
+ console.log('Instance 2:', instance2.instance);
206
+
207
+ console.assert(instance1.instance !== instance2.instance, 'Transient should return different instances');
208
+ console.assert(instance1.ready && instance2.ready, 'Transient instances should be ready immediately');
209
+
210
+ // Output:
211
+ // Building myTransientFactory...
212
+ // Building myTransientFactory...
213
+ // Instance 1: { id: '...', timestamp: ... }
214
+ // Instance 2: { id: '...', timestamp: ... }
215
+ }
216
+
217
+ runTransientExample();
218
+ ```
219
+
220
+ ### Reactive State Dependencies
221
+
222
+ Artifacts can react to changes in your global `DataStore` state by using `ctx.select()` within their factory's `use` block.
223
+
224
+ ```typescript
225
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
226
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
227
+
228
+ interface AppState {
229
+ theme: 'dark' | 'light';
230
+ notificationsEnabled: boolean;
231
+ }
232
+
233
+ const store = new ReactiveDataStore<AppState>({
234
+ theme: 'dark',
235
+ notificationsEnabled: true,
236
+ });
237
+ const container = new ArtifactContainer<any, AppState>(store);
238
+
239
+ let uiComponentBuilds = 0;
240
+ container.register({
241
+ key: 'uiConfiguration',
242
+ factory: async ({ use }) => {
243
+ uiComponentBuilds++;
244
+ const theme = await use((ctx) => ctx.select((state) => state.theme));
245
+ const notifications = await use((ctx) => ctx.select((state) => state.notificationsEnabled));
246
+
247
+ console.log(`UI Config Build ${uiComponentBuilds}: Theme=${theme}, Notifications=${notifications}`);
248
+ return { theme, notifications };
249
+ },
250
+ scope: ArtifactScope.Singleton,
251
+ });
252
+
253
+ async function runReactiveStateExample() {
254
+ // Initial resolution
255
+ let uiConfig = await container.resolve('uiConfiguration');
256
+ console.assert(uiConfig.instance?.theme === 'dark', 'Initial theme should be dark');
257
+ console.assert(uiComponentBuilds === 1, 'Initial build count should be 1');
258
+
259
+ // Simulate state change
260
+ console.log('\n--- Changing theme to light ---');
261
+ await store.set({ theme: 'light' });
262
+
263
+ // Resolve again - artifact should have been invalidated and rebuilt
264
+ uiConfig = await container.resolve('uiConfiguration');
265
+ console.assert(uiConfig.instance?.theme === 'light', 'Updated theme should be light');
266
+ console.assert(uiComponentBuilds === 2, 'Build count should increment after state change');
267
+
268
+ // Simulate another state change (different property)
269
+ console.log('\n--- Disabling notifications ---');
270
+ await store.set({ notificationsEnabled: false });
271
+
272
+ // Resolve again
273
+ uiConfig = await container.resolve('uiConfiguration');
274
+ console.assert(uiConfig.instance?.notificationsEnabled === false, 'Notifications should be false');
275
+ console.assert(uiComponentBuilds === 3, 'Build count should increment after another state change');
276
+
277
+ // Output:
278
+ // UI Config Build 1: Theme=dark, Notifications=true
279
+ //
280
+ // --- Changing theme to light ---
281
+ // UI Config Build 2: Theme=light, Notifications=true
282
+ //
283
+ // --- Disabling notifications ---
284
+ // UI Config Build 3: Theme=light, Notifications=false
285
+ }
286
+
287
+ runReactiveStateExample();
288
+ ```
289
+
290
+ ### Artifact-to-Artifact Dependencies
291
+
292
+ Artifacts can depend on other artifacts using `ctx.resolve()` within their factory's `use` block. Changes to a dependency will invalidate the dependent artifact.
293
+
294
+ ```typescript
295
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
296
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
297
+
298
+ const store = new ReactiveDataStore({});
299
+ const container = new ArtifactContainer(store);
300
+
301
+ let dbConnectionBuilds = 0;
302
+ container.register({
303
+ key: 'dbConnection',
304
+ factory: async () => {
305
+ dbConnectionBuilds++;
306
+ console.log(`Establishing DB Connection (${dbConnectionBuilds})...`);
307
+ await new Promise(res => setTimeout(res, 50)); // Simulate async work
308
+ return { client: 'PostgreSQL', status: 'connected' };
309
+ },
310
+ scope: ArtifactScope.Singleton,
311
+ });
312
+
313
+ let userRepositoryBuilds = 0;
314
+ container.register({
315
+ key: 'userRepository',
316
+ factory: async ({ use }) => {
317
+ userRepositoryBuilds++;
318
+ console.log(`Building UserRepository (${userRepositoryBuilds})...`);
319
+ const db = await use((ctx) => ctx.resolve('dbConnection'));
320
+ return {
321
+ dbClient: db.instance?.client,
322
+ findUser: (id: string) => `User ${id} from ${db.instance?.client}`
323
+ };
324
+ },
325
+ scope: ArtifactScope.Singleton,
326
+ });
327
+
328
+ async function runArtifactDependencyExample() {
329
+ // Initial resolution of UserRepository, which will resolve dbConnection first
330
+ let userRepo = await container.resolve('userRepository');
331
+ console.assert(dbConnectionBuilds === 1, 'DB Connection built once');
332
+ console.assert(userRepositoryBuilds === 1, 'User Repository built once');
333
+ console.log('User Repo initialized:', userRepo.instance?.findUser('1'));
334
+
335
+ // Manually invalidate the dbConnection. This will trigger a rebuild of both.
336
+ console.log('\n--- Invalidating DB Connection ---');
337
+ const dbConnection = await container.resolve('dbConnection');
338
+ await dbConnection.invalidate(true); // `true` forces immediate rebuild
339
+
340
+ // Resolve UserRepository again - it should have been rebuilt
341
+ userRepo = await container.resolve('userRepository');
342
+ console.assert(dbConnectionBuilds === 2, 'DB Connection rebuilt');
343
+ console.assert(userRepositoryBuilds === 2, 'User Repository rebuilt due to DB change');
344
+ console.log('User Repo after invalidation:', userRepo.instance?.findUser('2'));
345
+
346
+ // Output:
347
+ // Establishing DB Connection (1)...
348
+ // Building UserRepository (1)...
349
+ // User Repo initialized: User 1 from PostgreSQL
350
+ //
351
+ // --- Invalidating DB Connection ---
352
+ // Establishing DB Connection (2)...
353
+ // Building UserRepository (2)...
354
+ // User Repo after invalidation: User 2 from PostgreSQL
355
+ }
356
+
357
+ runArtifactDependencyExample();
358
+ ```
359
+
360
+ ### Lifecycle Hooks: `onCleanup` & `onDispose`
361
+
362
+ Artifacts can register cleanup functions to manage resources.
363
+
364
+ * `onCleanup`: Executed *before* a new instance of a `Singleton` artifact is built (due to invalidation) or when the artifact is explicitly unregistered. Ideal for releasing resources tied to a specific instance.
365
+ * `onDispose`: Executed *only* when the artifact is unregistered or the container itself is disposed. Ideal for final, permanent resource release.
366
+
367
+ ```typescript
368
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
369
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
370
+
371
+ const store = new ReactiveDataStore({});
372
+ const container = new ArtifactContainer(store);
373
+
374
+ let currentInterval: any;
375
+
376
+ container.register({
377
+ key: 'ticker',
378
+ factory: ({ onCleanup, onDispose }) => {
379
+ console.log('Ticker artifact created.');
380
+ let count = 0;
381
+ currentInterval = setInterval(() => {
382
+ count++;
383
+ console.log(`Tick: ${count}`);
384
+ }, 1000);
385
+
386
+ onCleanup(() => {
387
+ console.log('Cleaning up old Ticker instance (stopping interval)...');
388
+ clearInterval(currentInterval);
389
+ });
390
+
391
+ onDispose(() => {
392
+ console.log('Disposing Ticker artifact (final cleanup)...');
393
+ // Additional final cleanup could go here
394
+ });
395
+
396
+ return { start: Date.now() };
397
+ },
398
+ scope: ArtifactScope.Singleton,
399
+ lazy: false, // Eagerly build
400
+ });
401
+
402
+ async function runLifecycleHooksExample() {
403
+ await container.resolve('ticker');
404
+ console.log('Ticker running for 3 seconds...');
405
+ await new Promise(res => setTimeout(res, 3000));
406
+
407
+ console.log('\n--- Invalidating Ticker (will trigger onCleanup and rebuild) ---');
408
+ const ticker = await container.resolve('ticker'); // Get current to invalidate
409
+ await ticker.invalidate(true); // Force rebuild
410
+ console.log('Ticker rebuilt. Running for 2 more seconds...');
411
+ await new Promise(res => setTimeout(res, 2000));
412
+
413
+ console.log('\n--- Unregistering Ticker (will trigger onDispose) ---');
414
+ await container.unregister('ticker');
415
+ console.log('Ticker unregistered.');
416
+
417
+ // Output will show 'Cleaning up old Ticker instance' on invalidation,
418
+ // and 'Disposing Ticker artifact' on unregister.
419
+ }
420
+
421
+ runLifecycleHooksExample();
422
+ ```
423
+
424
+ ### Watching Artifacts for Changes
425
+
426
+ The `watch()` method provides a reactive way to observe an artifact's value without repeatedly calling `resolve()`. This is particularly useful for UI frameworks or reactive data flows.
427
+
428
+ ```typescript
429
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
430
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
431
+
432
+ interface AppState {
433
+ counter: number;
434
+ }
435
+
436
+ const store = new ReactiveDataStore<AppState>({ counter: 0 });
437
+ const container = new ArtifactContainer<any, AppState>(store);
438
+
439
+ container.register({
440
+ key: 'reactiveCounter',
441
+ factory: async ({ use }) => {
442
+ const count = await use((ctx) => ctx.select((state) => state.counter));
443
+ return `Current count: ${count}`;
444
+ },
445
+ scope: ArtifactScope.Singleton,
446
+ });
447
+
448
+ async function runWatcherExample() {
449
+ console.log('Setting up watcher...');
450
+ const watcher = container.watch('reactiveCounter');
451
+
452
+ let unsubscribe = watcher.subscribe(() => {
453
+ const resolved = watcher.get();
454
+ if (resolved.ready) {
455
+ console.log('Watcher received update:', resolved.instance);
456
+ } else if (resolved.error) {
457
+ console.error('Watcher received error:', resolved.error);
458
+ } else {
459
+ console.log('Watcher received pending update (artifact not ready yet).');
460
+ }
461
+ });
462
+
463
+ // Initial state should trigger a notification (pending, then ready)
464
+ await new Promise(res => setTimeout(res, 10)); // Give watcher time to resolve
465
+
466
+ console.log('\n--- Incrementing counter ---');
467
+ await store.set({ counter: 1 });
468
+ await new Promise(res => setTimeout(res, 10));
469
+
470
+ console.log('\n--- Incrementing counter again ---');
471
+ await store.set({ counter: 2 });
472
+ await new Promise(res => setTimeout(res, 10));
473
+
474
+ console.log('\n--- Disposing watcher ---');
475
+ unsubscribe(); // Unsubscribe first
476
+ await watcher.dispose(); // Then dispose the watcher itself
477
+
478
+ // Output:
479
+ // Setting up watcher...
480
+ // Watcher received pending update (artifact not ready yet).
481
+ // Watcher received update: Current count: 0
482
+ //
483
+ // --- Incrementing counter ---
484
+ // Watcher received update: Current count: 1
485
+ //
486
+ // --- Incrementing counter again ---
487
+ // Watcher received update: Current count: 2
488
+ //
489
+ // --- Disposing watcher ---
490
+ }
491
+
492
+ runWatcherExample();
493
+ ```
494
+
495
+ ### Yielding Intermediate Values
496
+
497
+ For long-running or multi-stage artifact factories, `ctx.yield()` allows a Singleton artifact to publish intermediate values. This updates the artifact's current instance and notifies dependents/watchers without fully completing the factory execution.
498
+
499
+ ```typescript
500
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
501
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
502
+
503
+ const store = new ReactiveDataStore({});
504
+ const container = new ArtifactContainer(store);
505
+
506
+ container.register({
507
+ key: 'longProcessArtifact',
508
+ factory: async ({ yield: publish }) => {
509
+ async function process() {
510
+ console.log('Starting long process...');
511
+
512
+ publish('Step 1: Initializing...');
513
+ await new Promise(res => setTimeout(res, 100));
514
+
515
+ publish('Step 2: Loading data...');
516
+ await new Promise(res => setTimeout(res, 100));
517
+
518
+ publish('Step 3: Processing data...');
519
+ await new Promise(res => setTimeout(res, 100));
520
+
521
+ console.log('Long process complete.');
522
+ }
523
+
524
+ process()
525
+ return 'Created factory...';
526
+ },
527
+ scope: ArtifactScope.Singleton,
528
+ });
529
+
530
+ async function runYieldExample() {
531
+ const watcher = container.watch('longProcessArtifact');
532
+ const unsubscribe = watcher.subscribe(() => {
533
+ const resolved = watcher.get();
534
+ if (resolved.instance) {
535
+ console.log('Watcher observed yield:', resolved.instance);
536
+ }
537
+ });
538
+
539
+ await container.resolve('longProcessArtifact'); // Trigger the factory
540
+ console.log('Factory finished.');
541
+
542
+ unsubscribe();
543
+ await watcher.dispose();
544
+
545
+ // Output will show intermediate values from `yield` as they occur,
546
+ // then the final result once the factory completes.
547
+ }
548
+
549
+ runYieldExample();
550
+ ```
551
+
552
+ ### Hierarchical Containers
553
+
554
+ Create child containers to manage dependencies in a nested, scoped manner. Child containers can resolve artifacts registered in their parent.
555
+
556
+ ```typescript
557
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
558
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
559
+
560
+ const store = new ReactiveDataStore({});
561
+ const parentContainer = new ArtifactContainer(store);
562
+
563
+ parentContainer.register({
564
+ key: 'globalConfig',
565
+ factory: () => ({ logLevel: 'info', serverUrl: 'api.example.com' }),
566
+ scope: ArtifactScope.Singleton,
567
+ });
568
+
569
+ const childContainer = parentContainer.createChild();
570
+
571
+ childContainer.register({
572
+ key: 'featureService',
573
+ factory: async ({ use }) => {
574
+ const config = await use((ctx) => ctx.resolve('globalConfig')); // Resolves from parent
575
+ return {
576
+ name: 'FeatureService',
577
+ apiUrl: `${config.instance?.serverUrl}/feature`
578
+ };
579
+ },
580
+ scope: ArtifactScope.Singleton,
581
+ });
582
+
583
+ async function runHierarchicalExample() {
584
+ const globalConfig = await parentContainer.resolve('globalConfig');
585
+ const featureService = await childContainer.resolve('featureService');
586
+
587
+ console.log('Global Config from parent:', globalConfig.instance);
588
+ console.log('Feature Service from child:', featureService.instance);
589
+
590
+ console.assert(globalConfig.instance?.logLevel === 'info', 'Parent artifact resolved');
591
+ console.assert(featureService.instance?.apiUrl === 'api.example.com/feature', 'Child resolved parent artifact');
592
+
593
+ // Output:
594
+ // Global Config from parent: { logLevel: 'info', serverUrl: 'api.example.com' }
595
+ // Feature Service from child: { name: 'FeatureService', apiUrl: 'api.example.com/feature' }
596
+ }
597
+
598
+ runHierarchicalExample();
599
+ ```
600
+
601
+ ### Debugging Information
602
+
603
+ Use `getDebugInfo()` to inspect the current state of artifacts within a container, including their status, dependencies, and dependents.
604
+
605
+ ```typescript
606
+ import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
607
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
608
+
609
+ const store = new ReactiveDataStore({});
610
+ const container = new ArtifactContainer(store);
611
+
612
+ container.register({
613
+ key: 'serviceA',
614
+ factory: () => 'Service A Instance',
615
+ scope: ArtifactScope.Singleton,
616
+ });
617
+
618
+ container.register({
619
+ key: 'serviceB',
620
+ factory: async ({ use }) => {
621
+ await use((ctx) => ctx.resolve('serviceA'));
622
+ return 'Service B Instance';
623
+ },
624
+ scope: ArtifactScope.Singleton,
625
+ });
626
+
627
+ async function runDebugInfoExample() {
628
+ await container.resolve('serviceB'); // Resolve to build dependencies
629
+
630
+ const debugInfo = container.getDebugInfo();
631
+ console.log('--- Artifact Debug Information ---');
632
+ debugInfo.forEach(node => {
633
+ console.log(`ID: ${node.id}`);
634
+ console.log(` Scope: ${node.scope}`);
635
+ console.log(` Status: ${node.status}`);
636
+ console.log(` Dependencies: ${node.dependencies.join(', ') || 'None'}`);
637
+ console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
638
+ console.log(` Render Count: ${node.renderCount}`);
639
+ console.log('---');
640
+ });
641
+
642
+ // Example output for 'serviceB':
643
+ // ID: serviceB
644
+ // Scope: singleton
645
+ // Status: active
646
+ // Dependencies: serviceA
647
+ // Dependents: None
648
+ // Render Count: 1
649
+ }
650
+
651
+ runDebugInfoExample();
652
+ ```
653
+
654
+ ---
655
+
656
+ ## 🏛️ Project Architecture
657
+
658
+ The `@asaidimu/utils-artifacts` package is built around the central `ArtifactContainer` class, which orchestrates the lifecycle and dependencies of various "artifacts" (any component or value you register).
659
+
660
+ ### Core Components
661
+
662
+ * **`ArtifactContainer`**: The heart of the system. It manages the registration, resolution, lifecycle (Singleton/Transient), and reactive updates of all artifacts. It can also create child containers to form a hierarchy.
663
+ * **`ArtifactDefinition`**: An internal representation of a registered artifact, holding its configuration (scope, lazy loading, retries, etc.) and runtime state (current instance, error, cleanup functions, dependency graph links).
664
+ * **`ArtifactFactory`**: A function that defines how an artifact instance is created. It receives an `ArtifactFactoryContext` which is crucial for declaring dependencies and managing its lifecycle.
665
+ * **`DataStore` (External Dependency)**: The container relies on an external data store (e.g., `@asaidimu/utils-store`) to manage global application state. The container interacts with it via `watch` (for reactive subscriptions) and `get` (for reading current state). The `buildPaths` function is used internally to derive specific state paths for granular dependency tracking.
666
+ * **`ArtifactWatcher`**: An observable interface for tracking the resolved state of an artifact over time, providing `get` for current value and `subscribe` for updates.
667
+
668
+ ### Data Flow
669
+
670
+ 1. **Registration (`container.register`)**: An `ArtifactFactory` is associated with a `key` and `ArtifactOptions` (scope, lazy, retries, etc.).
671
+ 2. **Resolution (`container.resolve`)**:
672
+ * If the artifact is a `Singleton` and already built, the cached instance is returned.
673
+ * If not built, the `ArtifactFactory` is invoked within a specialized context.
674
+ * **Dependency Declaration (`ctx.use`)**: Inside the factory, `ctx.use` provides:
675
+ * `ctx.resolve(key)`: Declares a dependency on another artifact. This call recursively triggers resolution of the dependency.
676
+ * `ctx.select(selector)`: Declares a dependency on a slice of the global `DataStore` state. Internally, `buildPaths` extracts specific state paths from the selector.
677
+ 3. **Dependency Graph Construction**: As dependencies are declared, the `ArtifactContainer` builds a precise, bidirectional dependency graph, linking artifacts to their dependents and subscribed state paths.
678
+ 4. **Reactive Invalidation**:
679
+ * **State Changes**: When a `DataStore` value changes, its `watch` callback is invoked. If the changed path matches an `ArtifactDefinition`'s `stateDependencies`, `container.invalidate()` is called for that artifact.
680
+ * **Artifact Changes**: When an artifact is rebuilt or `yield()`s a new value, its `dependents` are identified and `container.invalidate()` is called for each.
681
+ 5. **Rebuild Process**: `container.invalidate()` triggers:
682
+ * **Debouncing**: If configured, rebuilds are debounced to consolidate multiple rapid invalidations into a single rebuild.
683
+ * **Cleanup**: `onCleanup` hooks for the old instance are run.
684
+ * **New Instance**: The `ArtifactFactory` is re-executed, creating a new instance.
685
+ * **Graph Update**: The dependency graph is updated based on the new factory execution.
686
+ * **Notification**: `ArtifactWatcher` subscribers are notified of the new instance.
687
+ 6. **Disposal (`container.dispose` / `container.unregister`)**: All resources (`onDispose` hooks, state subscriptions, instance cache) are released.
688
+
689
+ ### Extension Points
690
+
691
+ The primary extension point is the **`ArtifactFactory` function**. By implementing custom factories, you can:
692
+ * Integrate with any third-party library or framework.
693
+ * Define complex business logic.
694
+ * Manage external resources like database connections, API clients, or message queues.
695
+ * Provide custom logging, monitoring, or telemetry by injecting those services as dependencies.
696
+
697
+ The `ArtifactContainer` itself is designed to be highly configurable via `ArtifactOptions`, allowing fine-tuning of scope, lazy loading, error handling, and reactive behavior.
698
+
699
+ ---
700
+
701
+ ## 👨‍💻 Development & Contributing
702
+
703
+ We welcome contributions! Please read the guidelines below.
704
+
705
+ ### Development Setup
706
+
707
+ 1. **Clone the repository**:
708
+ ```bash
709
+ git clone https://github.com/asaidimu/utils.git
710
+ cd utils/src/artifacts
711
+ ```
712
+ 2. **Install dependencies**:
713
+ ```bash
714
+ bun install
715
+ # or npm install
716
+ # or yarn install
717
+ ```
718
+ 3. **Build (if necessary for changes outside `src`)**:
719
+ ```bash
720
+ bun run build
721
+ ```
722
+
723
+ ### Scripts
724
+
725
+ * `bun install`: Installs project dependencies.
726
+ * `bun run test`: Runs the test suite using Vitest.
727
+ * `bun run build`: Compiles the TypeScript source code into JavaScript.
728
+
729
+ ### Testing
730
+
731
+ This project uses [Vitest](https://vitest.dev/) for testing.
732
+
733
+ * To run all tests:
734
+ ```bash
735
+ bun run test
736
+ ```
737
+ * To run tests in watch mode:
738
+ ```bash
739
+ bun run test --watch
740
+ ```
741
+ * Ensure all new features or bug fixes are accompanied by appropriate unit and/or integration tests.
742
+
743
+ ### Contributing Guidelines
744
+
745
+ We appreciate your interest in contributing to `@asaidimu/utils-artifacts`!
746
+ Please refer to the main repository's [CONTRIBUTING.md](https://github.com/asaidimu/utils/blob/main/CONTRIBUTING.md) for detailed guidelines on:
747
+
748
+ * Reporting issues
749
+ * Submitting pull requests
750
+ * Coding standards
751
+ * Commit message conventions (we follow Conventional Commits)
752
+
753
+ ### Issue Reporting
754
+
755
+ Found a bug or have a feature request? Please open an issue on our [GitHub Issues page](https://github.com/asaidimu/utils/issues).
756
+ Provide as much detail as possible, including steps to reproduce, expected behavior, and your environment.
757
+
758
+ ---
759
+
760
+ ## ➕ Additional Information
761
+
762
+ ### Troubleshooting
763
+
764
+ * **`ArtifactNotFoundError`**: This typically means you're trying to `resolve` an artifact that hasn't been `register`ed in the current container or any of its parent containers. Double-check your `key`s and registration calls.
765
+ * **`CircularDependencyError`**: Occurs when artifact A depends on B, and B depends back on A (directly or indirectly). This indicates a structural problem in your dependency graph. Redesign your artifacts to break the cycle. The error message will show the path of the cycle.
766
+ * **Artifact Not Updating Reactively**:
767
+ * Ensure your artifact is a `Singleton` (Transient artifacts don't cache or react to invalidations).
768
+ * Verify you are using `ctx.use()` and `ctx.select()` (for state) or `ctx.resolve()` (for other artifacts) inside your factory to declare dependencies.
769
+ * Check that your `DataStore` implementation is correctly triggering its `watch` callbacks.
770
+ * **`IllegalScopeError`**: You might be trying to `yield` a value from a `Transient` artifact. `yield` is only meaningful for `Singleton` artifacts, which maintain a persistent instance.
771
+ * **Timeout Errors**: Your artifact factory took longer than `timeoutMs` to complete. Consider increasing the `timeoutMs` or optimizing your factory's logic.
772
+
773
+ ### FAQ
774
+
775
+ * **What is an "artifact" in this context?**
776
+ An artifact is simply any JavaScript/TypeScript value, object, or function that you want the `ArtifactContainer` to manage. This could be a service, a repository, a configuration object, a computed value, or even a simple primitive.
777
+ * **Why use this over a simpler DI library?**
778
+ `@asaidimu/utils-artifacts` goes beyond basic DI by adding a powerful reactive layer. It automatically handles the propagation of changes from global state or other artifacts, making it easier to build self-updating components and reactive systems without manual subscription management.
779
+ * **When should I use `Singleton` vs. `Transient`?**
780
+ * **Singleton**: For services, database connections, configuration objects, or any resource that should be shared and consistently available throughout your application. They are efficient as they are built once and reused.
781
+ * **Transient**: For objects that need a fresh instance every time they are requested, such as temporary calculators, request-scoped contexts, or factory functions that produce unique outputs.
782
+ * **How does `debounce` work?**
783
+ `debounce` prevents an artifact from rebuilding too frequently. If multiple invalidations occur within the specified `debounce` time, they are consolidated, and the artifact's factory is only run once after the activity ceases. This is crucial for performance with rapid state changes.
784
+
785
+ ### Changelog / Roadmap
786
+
787
+ * **Changelog**: For a detailed list of changes, please refer to the [CHANGELOG.md](https://github.com/asaidimu/utils/blob/main/CHANGELOG.md) file in the main repository.
788
+ * **Roadmap**: Future plans and upcoming features are typically outlined in the main repository's [issue tracker](https://github.com/asaidimu/utils/issues) or project boards.
789
+
790
+ ### License
791
+
792
+ This project is licensed under the MIT License. See the [LICENSE](https://github.com/asaidimu/utils/blob/main/LICENSE) file for details.
793
+
794
+ ### Acknowledgments
795
+
796
+ * Developed as part of the `@asaidimu` utilities collection.
797
+ * Uses [Semantic Release](https://semantic-release.gitbook.io/semantic-release/) for automated versioning and publishing.
798
+ * Leverages [Vitest](https://vitest.dev/) for fast and reliable testing.
799
+