@asaidimu/utils-artifacts 5.0.0 → 5.0.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 (4) hide show
  1. package/README.md +528 -605
  2. package/index.js +1 -1
  3. package/index.mjs +1 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,52 +1,51 @@
1
- # `@asaidimu/utils-artifacts`
1
+ # @asaidimu/utils-artifacts
2
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.
3
+ A powerful TypeScript library for managing application components (artifacts) with dependency injection, reactive state updates, and robust lifecycle management. This library provides a flexible container to define, resolve, and orchestrate various parts of your application, each reacting dynamically to state changes and dependencies.
4
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://github.com/asaidimu/erp-utils/blob/main/LICENSE)
7
- [![Build Status](https://img.shields.io/github/actions/workflow/status/asaidimu/erp-utils/ci.yml?branch=main&label=Build&style=flat-square)](https://github.com/asaidimu/erp-utils/actions?query=workflow%3ACI)
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/erp-utils/ci.yml?branch=main&style=flat-square)](https://github.com/asaidimu/erp-utils/actions/workflows/ci.yml)
8
8
 
9
9
  ---
10
10
 
11
- ### Quick Links
12
-
13
- * [Overview & Features](#-overview--features)
14
- * [Installation & Setup](#-installation--setup)
15
- * [Usage Documentation](#-usage-documentation)
16
- * [Basic Artifacts (Singleton & Transient)](#basic-artifacts-singleton--transient)
17
- * [Reactive State Dependencies](#reactive-state-dependencies)
18
- * [Artifact-to-Artifact Dependencies](#artifact-to-artifact-dependencies)
19
- * [Lifecycle Hooks: `onCleanup` & `onDispose`](#lifecycle-hooks-oncleanup--ondispose)
20
- * [Watching Artifacts for Changes](#watching-artifacts-for-changes)
21
- * [Streaming Artifacts with `ctx.stream()`](#streaming-artifacts-with-ctxstream)
22
- * [Debugging Information](#debugging-information)
23
- * [Project Architecture](#-project-architecture)
24
- * [Development & Contributing](#-development--contributing)
25
- * [Additional Information](#-additional-information)
11
+ ## 🚀 Quick Links
12
+
13
+ - [Overview & Features](#-overview--features)
14
+ - [Installation & Setup](#-installation--setup)
15
+ - [Usage Documentation](#-usage-documentation)
16
+ - [Basic Usage](#basic-usage)
17
+ - [Registering Artifacts](#registering-artifacts)
18
+ - [Resolving & Requiring Artifacts](#resolving--requiring-artifacts)
19
+ - [Watching Artifact Changes](#watching-artifact-changes)
20
+ - [Reactive Dependencies (State Selection)](#reactive-dependencies-state-selection)
21
+ - [Artifact Lifecycle (Cleanup & Dispose)](#artifact-lifecycle-cleanup--dispose)
22
+ - [Streaming Artifacts](#streaming-artifacts)
23
+ - [Invalidating Artifacts](#invalidating-artifacts)
24
+ - [Debugging Artifacts](#debugging-artifacts)
25
+ - [Retry Utility](#retry-utility)
26
+ - [Concurrency Utilities (Once & Serializer)](#concurrency-utilities-once--serializer)
27
+ - [Project Architecture](#-project-architecture)
28
+ - [Development & Contributing](#-development--contributing)
29
+ - [Additional Information](#-additional-information)
26
30
 
27
31
  ---
28
32
 
29
- ⚠️ **Beta Warning**
30
- 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!
33
+ ## Overview & Features
31
34
 
32
- ## 📚 Overview & Features
35
+ `@asaidimu/utils-artifacts` is a reactive dependency injection container designed to bring order and efficiency to complex application architectures. It allows you to define application components (called "artifacts") as factories that declare their dependencies on other artifacts and global application state. The container automatically manages the lifecycle, instantiation, and invalidation of these artifacts, ensuring that components are always up-to-date with their dependencies.
33
36
 
34
- `@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.
37
+ ### Key Features
35
38
 
36
- 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.
37
-
38
- ### Key Features
39
-
40
- * **Reactive Dependency Injection**: Automatically invalidates and rebuilds `Singleton` artifacts when their declared dependencies (other artifacts or global state slices) change, ensuring reactive updates throughout your application.
41
- * **Singleton & Transient Scopes**: Control whether an artifact is instantiated once and reused (`Singleton`) or created fresh on every request (`Transient`).
42
- * **State-Driven Invalidation with Debouncing**: Integrates with a `DataStore` (e.g., `@asaidimu/utils-store`) to track state dependencies, triggering artifact rebuilds only when relevant state paths change, with configurable debouncing to prevent excessive rebuilds.
43
- * **Fine-grained Dependency Tracking**: Explicitly declare dependencies on other artifacts using `ctx.use(({ resolve }) => resolve('someKey'))` and on state slices using `ctx.use(({ select }) => select(state => state.somePath))`.
44
- * **Robust Error Handling**: Built-in detection and specific error types for circular dependencies (`CircularDependencyError`), missing artifacts (`ArtifactNotFoundError`), illegal operations (`IllegalScopeError`), and timeouts (`TimeoutError`), distinguishing between `system` (structural) and `external` (runtime) errors.
45
- * **Lifecycle Hooks**: Register `onCleanup` callbacks for instance-specific resource release (executed before a `Singleton` instance is rebuilt) and `onDispose` for final resource teardown (executed when the artifact is unregistered).
46
- * **Asynchronous Artifact Resolution**: Supports `async`/`await` in artifact factories, with configurable `retries` for external failures and `timeout` settings for long-running operations.
47
- * **`ArtifactObserver` for Reactive Observation**: Use `container.watch(key)` to get an observer that allows subscribing to an artifact's resolved value over time, providing reactive updates to UI components or other systems.
48
- * **Live Streaming with `ctx.stream()`**: `Singleton` artifacts can emit multiple values over their lifetime from within their factory using `ctx.stream()`, immediately notifying dependents and watchers of new data. This is ideal for continuous data sources like WebSockets or background processes.
49
- * **Comprehensive Debugging Info**: `container.debugInfo()` provides a runtime snapshot of the container's state, including artifact statuses, dependencies (artifact-to-artifact and state-to-artifact), and dependents.
39
+ * **Dependency Injection (DI)**: Declare dependencies within artifact factories using `ctx.use`, `ctx.resolve`, and `ctx.require`.
40
+ * **Reactive State Management**: Automatically re-evaluate artifacts when slices of a global `DataStore` (or any compatible state management system) change via `ctx.select`.
41
+ * **Flexible Scoping**: Supports `Singleton` (single, cached instance) and `Transient` (new instance per resolution) artifact scopes.
42
+ * **Advanced Lifecycle Management**: `onCleanup` for instance-specific cleanup and `onDispose` for permanent resource release.
43
+ * **Streaming Artifacts**: Use `ctx.stream` to build long-lived artifacts that continuously emit new values, ideal for real-time data, web sockets, or periodic tasks.
44
+ * **Robust Error Handling & Retries**: Integrated retry logic (`Retry` utility) for resilient operations and a comprehensive `ArtifactError` hierarchy for clear diagnostics.
45
+ * **Concurrency Primitives**: Internal `Once` and `Serializer` utilities for managing race conditions and sequential execution of async operations.
46
+ * **Debuggability**: `container.debugInfo()` provides a snapshot of the artifact graph, statuses, and dependencies for easy troubleshooting.
47
+ * **Watcher API**: `container.watch()` allows external consumers to subscribe to artifact value changes without directly resolving them.
48
+ * **Circular Dependency Detection**: Prevents infinite loops during resolution by detecting and reporting cycles in the dependency graph.
50
49
 
51
50
  ---
52
51
 
@@ -54,761 +53,688 @@ This package is ideal for building complex, event-driven systems, front-end appl
54
53
 
55
54
  ### Prerequisites
56
55
 
57
- * **Node.js**: (v18 or higher recommended)
58
- * **Package Manager**: `Bun`, `npm`, or `Yarn`.
59
- * **DataStore Implementation**: An external implementation of a `DataStore` interface with `get` and `watch` methods. We highly recommend using [`@asaidimu/utils-store`](https://www.npmjs.com/package/@asaidimu/utils-store) as it is designed for seamless integration.
56
+ * Node.js (LTS recommended)
57
+ * npm or Yarn (package manager)
58
+ * A reactive data store library that adheres to the `DataStore` interface (e.g., `@asaidimu/utils-store`).
60
59
 
61
60
  ### Installation Steps
62
61
 
63
- Install the package using your preferred package manager:
62
+ To install the library, use your preferred package manager:
64
63
 
65
64
  ```bash
66
- # With Bun
67
- bun add @asaidimu/utils-artifacts
68
-
69
- # With npm
70
65
  npm install @asaidimu/utils-artifacts
71
-
72
- # With Yarn
66
+ # or
73
67
  yarn add @asaidimu/utils-artifacts
74
68
  ```
75
69
 
76
70
  ### Configuration
77
71
 
78
- To use the `ArtifactContainer`, you need to initialize it with an object that provides `watch` (for reactive subscriptions to state changes), `get` (for reading the current state), and `set` (for allowing artifacts to update state, e.g., via `ctx.stream` or `ctx.set`).
72
+ `@asaidimu/utils-artifacts` is a library and does not require global configuration files. Its primary setup involves initializing the `ArtifactContainer` with an instance of a `DataStore`.
79
73
 
80
- If you're using `@asaidimu/utils-store` as your `DataStore`:
74
+ ### Verification
75
+
76
+ You can verify the installation by attempting to import and register a basic artifact:
81
77
 
82
78
  ```typescript
83
79
  import { ArtifactContainer } from '@asaidimu/utils-artifacts';
84
- import { ReactiveDataStore } from '@asaidimu/utils-store'; // Recommended DataStore
80
+ import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming you have this store
85
81
 
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
- };
82
+ // Define your application's global state and artifact registry types
83
+ type AppState = { count: number; };
84
+ type AppRegistry = { myService: string; };
97
85
 
98
- const store = new ReactiveDataStore<AppState>(initialAppState);
99
-
100
- // Initialize the ArtifactContainer
101
- // The generic arguments <TRegistry, TState> are inferred or can be explicitly provided.
102
- const container = new ArtifactContainer<Record<string, any>, AppState>(store);
103
-
104
- // Now you can start registering artifacts
105
- ```
86
+ const store = new ReactiveDataStore<AppState>({ count: 0 });
87
+ const container = new ArtifactContainer<AppRegistry, AppState>(store);
106
88
 
107
- ### Verification
89
+ container.register({
90
+ key: 'myService',
91
+ factory: async ({ use }) => {
92
+ const currentCount = await use(({ select }) => select(s => s.count));
93
+ return `Service is running with count: ${currentCount}`;
94
+ },
95
+ });
108
96
 
109
- You can verify the installation by trying to import and instantiate the `ArtifactContainer`:
97
+ async function runExample() {
98
+ const service = await container.resolve('myService');
99
+ console.log(service.instance); // Expected: Service is running with count: 0
100
+ }
110
101
 
111
- ```typescript
112
- import { ArtifactContainer } from '@asaidimu/utils-artifacts';
113
- // Assuming a mock or real DataStore is available
114
- const mockStore = {
115
- get: () => ({}), // Returns an empty object for state
116
- watch: () => () => {}, // Returns a no-op unsubscribe function
117
- set: async () => ({}), // Returns a no-op set function
118
- };
119
- const container = new ArtifactContainer(mockStore);
120
- console.log('ArtifactContainer initialized successfully!');
121
- // If no errors, the installation is successful.
102
+ runExample();
122
103
  ```
123
104
 
124
105
  ---
125
106
 
126
- ## 💡 Usage Documentation
107
+ ## 📖 Usage Documentation
127
108
 
128
- ### Basic Artifacts (Singleton & Transient)
109
+ ### Basic Usage
129
110
 
130
- Artifacts can be registered with different scopes, controlling their lifecycle and instantiation behavior.
131
-
132
- #### Singleton Scope
133
-
134
- A `Singleton` artifact is created only once. Subsequent `resolve` calls or reactive updates will return or use the same instance, unless its dependencies change, which triggers a rebuild. This is the default scope.
111
+ The core of the library is the `ArtifactContainer`. You initialize it with a `DataStore` that manages your application's global state.
135
112
 
136
113
  ```typescript
137
114
  import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
138
115
  import { ReactiveDataStore } from '@asaidimu/utils-store';
139
116
 
140
- const store = new ReactiveDataStore({});
141
- const container = new ArtifactContainer(store);
117
+ // 1. Define your application's global state and artifact registry types
118
+ interface AppState {
119
+ appName: string;
120
+ version: string;
121
+ config: {
122
+ theme: 'light' | 'dark';
123
+ apiUrl: string;
124
+ };
125
+ }
126
+
127
+ interface AppRegistry {
128
+ logger: LoggerService;
129
+ apiClient: ApiClient;
130
+ themeService: 'light' | 'dark';
131
+ appInfo: string;
132
+ }
133
+
134
+ // Example DataStore (from @asaidimu/utils-store)
135
+ const appStore = new ReactiveDataStore<AppState>({
136
+ appName: 'My App',
137
+ version: '1.0.0',
138
+ config: {
139
+ theme: 'light',
140
+ apiUrl: 'https://api.example.com',
141
+ },
142
+ });
142
143
 
143
- let buildCount = 0;
144
+ // 2. Instantiate the ArtifactContainer
145
+ const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
146
+
147
+ // 3. Define and register your artifact factories
148
+ class LoggerService {
149
+ constructor(private prefix: string) {}
150
+ log(message: string) {
151
+ console.log(`[${this.prefix}] ${message}`);
152
+ }
153
+ }
154
+
155
+ container.register({
156
+ key: 'logger',
157
+ scope: ArtifactScopes.Singleton, // Ensure only one instance of the logger
158
+ factory: () => new LoggerService('APP'),
159
+ });
144
160
 
145
161
  container.register({
146
- key: 'mySingletonService',
147
- factory: () => {
148
- buildCount++;
149
- console.log('Building mySingletonService...');
162
+ key: 'apiClient',
163
+ scope: ArtifactScopes.Singleton,
164
+ factory: async ({ use }) => {
165
+ // ApiClient depends on the logger and state.config.apiUrl
166
+ const logger = await use(({ require }) => require('logger'));
167
+ const apiUrl = await use(({ select }) => select(state => state.config.apiUrl));
168
+
169
+ logger.log(`Initializing API client for ${apiUrl}`);
150
170
  return {
151
- name: 'SingletonService',
152
- version: '1.0.0'
171
+ fetchData: async (path: string) => {
172
+ logger.log(`Fetching from ${apiUrl}${path}`);
173
+ // Simulate API call
174
+ await new Promise(res => setTimeout(res, 100));
175
+ return { message: `Data from ${apiUrl}${path}` };
176
+ }
153
177
  };
154
178
  },
179
+ });
180
+
181
+ container.register({
182
+ key: 'themeService',
155
183
  scope: ArtifactScopes.Singleton,
156
- lazy: false, // Build immediately on registration if it's a Singleton
184
+ factory: async ({ use }) => {
185
+ // Theme service depends directly on state
186
+ return await use(({ select }) => select(state => state.config.theme));
187
+ },
157
188
  });
158
189
 
159
- async function runSingletonExample() {
160
- const service1 = await container.resolve('mySingletonService');
161
- const service2 = await container.resolve('mySingletonService');
190
+ container.register({
191
+ key: 'appInfo',
192
+ scope: ArtifactScopes.Singleton,
193
+ factory: async ({ use }) => {
194
+ // App info depends on other artifacts and state
195
+ const logger = await use(({ require }) => require('logger'));
196
+ const apiClient = await use(({ require }) => require('apiClient'));
197
+ const appName = await use(({ select }) => select(s => s.appName));
198
+
199
+ const data = await apiClient.fetchData('/info');
200
+ logger.log(`App Info: ${appName}, Data: ${data.message}`);
201
+ return `App: ${appName}, API Data: ${data.message}`;
202
+ },
203
+ });
204
+
205
+ // 4. Resolve artifacts to use them
206
+ async function main() {
207
+ const logger = await container.resolve('logger');
208
+ logger.instance?.log('Application started.');
162
209
 
163
- console.log('Service 1:', service1.instance);
164
- console.log('Service 2:', service2.instance);
210
+ const appInfo = await container.resolve('appInfo');
211
+ console.log(appInfo.instance); // Logs the app information after API call
165
212
 
166
- console.assert(buildCount === 1, 'Singleton factory should be called once');
167
- console.assert(service1.instance === service2.instance, 'Both resolutions should return the same instance');
168
- console.assert(service1.ready, 'Singleton should be ready');
213
+ // Example of reacting to state changes
214
+ console.log('\n--- Changing theme ---');
215
+ await appStore.set(s => ({ ...s, config: { ...s.config, theme: 'dark' } }));
169
216
 
170
- // Output:
171
- // Building mySingletonService...
172
- // Service 1: { name: 'SingletonService', version: '1.0.0' }
173
- // Service 2: { name: 'SingletonService', version: '1.0.0' }
217
+ // Because themeService depends on state.config.theme, it will be rebuilt
218
+ // Any artifact that depends on themeService would also be rebuilt.
219
+ const newTheme = await container.resolve('themeService');
220
+ console.log('New Theme:', newTheme.instance); // Expected: dark
174
221
  }
175
222
 
176
- runSingletonExample();
223
+ main();
177
224
  ```
178
225
 
179
- #### Transient Scope
226
+ ### Registering Artifacts
180
227
 
181
- A `Transient` artifact creates a new instance every time it is resolved. These artifacts do not participate in caching, reactive invalidation, or lifecycle hooks like `onCleanup` and `onDispose`.
228
+ Use `container.register()` to define an artifact. Each artifact requires a unique `key` and a `factory` function.
182
229
 
183
230
  ```typescript
184
- import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
185
- import { ReactiveDataStore } from '@asaidimu/utils-store';
186
-
187
- const store = new ReactiveDataStore({});
188
- const container = new ArtifactContainer(store);
231
+ import { ArtifactScopes } from '@asaidimu/utils-artifacts';
189
232
 
190
233
  container.register({
191
- key: 'myTransientFactory',
192
- factory: () => {
193
- console.log('Building myTransientFactory...');
194
- return {
195
- id: Math.random().toString(36).substring(7),
196
- timestamp: Date.now()
197
- };
198
- },
199
- scope: ArtifactScopes.Transient,
234
+ key: 'myArtifact',
235
+ factory: () => 'Hello, Artifact!',
236
+ // Optional parameters:
237
+ scope: ArtifactScopes.Singleton, // 'singleton' (default) or 'transient'
238
+ lazy: true, // true (default) for singletons, false to build on registration
239
+ timeout: 5000, // Max time in ms for factory to complete
240
+ retries: 3, // Number of retries on factory failure
241
+ debounce: 100, // Delay in ms for rebuilding on dependency changes
200
242
  });
243
+ ```
201
244
 
202
- async function runTransientExample() {
203
- const instance1 = await container.resolve('myTransientFactory');
204
- const instance2 = await container.resolve('myTransientFactory');
205
-
206
- console.log('Instance 1:', instance1.instance);
207
- console.log('Instance 2:', instance2.instance);
208
-
209
- console.assert(instance1.instance !== instance2.instance, 'Transient should return different instances');
210
- console.assert(instance1.ready && instance2.ready, 'Transient instances should be ready immediately');
245
+ The `factory` function receives an `ArtifactFactoryContext` object:
211
246
 
212
- // Output:
213
- // Building myTransientFactory...
214
- // Building myTransientFactory...
215
- // Instance 1: { id: '...', timestamp: ... }
216
- // Instance 2: { id: '...', timestamp: ... }
247
+ ```typescript
248
+ interface ArtifactFactoryContext<TRegistry, TState, TArtifact> {
249
+ state(): TState; // Get current global state (non-reactive)
250
+ previous?: TArtifact; // Previous instance (for singletons on rebuild)
251
+ use<R>(callback: (ctx: UseDependencyContext<TRegistry, TState>) => R | Promise<R>): Promise<R>;
252
+ onCleanup(cleanup: ArtifactCleanup): void; // Register cleanup for current instance
253
+ onDispose(callback: ArtifactCleanup): void; // Register cleanup for artifact (permanent)
254
+ stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => ...): void; // Start streaming values (singletons only)
217
255
  }
218
256
 
219
- runTransientExample();
257
+ interface UseDependencyContext<TRegistry, TState> {
258
+ resolve<K extends keyof TRegistry>(key: K): Promise<ResolvedArtifact<TRegistry[K]>>; // Resolve an artifact (returns ResolvedArtifact)
259
+ require<K extends keyof TRegistry>(key: K): Promise<TRegistry[K]>; // Resolve an artifact (throws on error, returns instance directly)
260
+ select<S>(selector: (state: TState) => S): S; // Select state slice (reactive)
261
+ }
220
262
  ```
221
263
 
222
- ### Reactive State Dependencies
264
+ ### Resolving & Requiring Artifacts
223
265
 
224
- Artifacts can react to changes in your global `DataStore` state by using `ctx.use(({ select }) => ...)` within their factory. When the selected state slice changes, the `Singleton` artifact is automatically invalidated and rebuilt.
266
+ * `container.resolve(key)`: Returns a `Promise<ResolvedArtifact<T>>`. `ResolvedArtifact` is a union type that can be `ReadyArtifact`, `ErrorArtifact`, or `PendingArtifact`. You should check the `ready` and `error` properties.
267
+ * `container.require(key)`: Returns a `Promise<T>` directly. If resolution fails or the artifact has an error, it will throw the error. Use this when you are certain the artifact will resolve successfully.
225
268
 
226
269
  ```typescript
227
- import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
228
- import { ReactiveDataStore } from '@asaidimu/utils-store';
270
+ import { ArtifactError } from '@asaidimu/utils-artifacts';
271
+
272
+ // Using resolve (recommended for robust error handling)
273
+ const myArtifactResult = await container.resolve('myArtifact');
274
+ if (myArtifactResult.ready) {
275
+ console.log('Artifact instance:', myArtifactResult.instance);
276
+ } else if (myArtifactResult.error) {
277
+ console.error('Artifact failed to resolve:', myArtifactResult.error);
278
+ } else {
279
+ console.log('Artifact is pending/idle.');
280
+ }
229
281
 
230
- interface AppState {
231
- theme: 'dark' | 'light';
232
- notificationsEnabled: boolean;
282
+ // Using require (for simpler usage when errors are handled upstream or unexpected)
283
+ try {
284
+ const myArtifactInstance = await container.require('myArtifact');
285
+ console.log('Artifact instance:', myArtifactInstance);
286
+ } catch (error) {
287
+ if (error instanceof ArtifactError) {
288
+ console.error('Artifact system error:', error.message);
289
+ } else {
290
+ console.error('Artifact runtime error:', error);
291
+ }
233
292
  }
293
+ ```
234
294
 
235
- const store = new ReactiveDataStore<AppState>({
236
- theme: 'dark',
237
- notificationsEnabled: true,
238
- });
239
- const container = new ArtifactContainer<any, AppState>(store);
295
+ ### Watching Artifact Changes
240
296
 
241
- let uiComponentBuilds = 0;
242
- container.register({
243
- key: 'uiConfiguration',
244
- factory: async ({ use }) => {
245
- uiComponentBuilds++;
246
- const theme = await use((ctx) => ctx.select((state) => state.theme));
247
- const notifications = await use((ctx) => ctx.select((state) => state.notificationsEnabled));
297
+ The `watch()` method provides an observer pattern to react to artifact changes without needing to repeatedly call `resolve()`. It's particularly useful for UI frameworks.
248
298
 
249
- console.log(`UI Config Build ${uiComponentBuilds}: Theme=${theme}, Notifications=${notifications}`);
250
- return { theme, notifications };
251
- },
252
- scope: ArtifactScopes.Singleton,
253
- });
299
+ ```typescript
300
+ const myServiceWatcher = container.watch('myService');
254
301
 
255
- async function runReactiveStateExample() {
256
- // Initial resolution
257
- let uiConfig = await container.resolve('uiConfiguration');
258
- console.assert(uiConfig.instance?.theme === 'dark', 'Initial theme should be dark');
259
- console.assert(uiComponentBuilds === 1, 'Initial build count should be 1');
260
-
261
- // Simulate state change
262
- console.log('\n--- Changing theme to light ---');
263
- await store.set({ theme: 'light' });
264
-
265
- // Resolve again - artifact should have been invalidated and rebuilt
266
- uiConfig = await container.resolve('uiConfiguration');
267
- console.assert(uiConfig.instance?.theme === 'light', 'Updated theme should be light');
268
- console.assert(uiComponentBuilds === 2, 'Build count should increment after state change');
269
-
270
- // Simulate another state change (different property)
271
- console.log('\n--- Disabling notifications ---');
272
- await store.set({ notificationsEnabled: false });
273
-
274
- // Resolve again
275
- uiConfig = await container.resolve('uiConfiguration');
276
- console.assert(uiConfig.instance?.notifications === false, 'Notifications should be false');
277
- console.assert(uiComponentBuilds === 3, 'Build count should increment after another state change');
278
-
279
- // Output:
280
- // UI Config Build 1: Theme=dark, Notifications=true
281
- //
282
- // --- Changing theme to light ---
283
- // UI Config Build 2: Theme=light, Notifications=true
284
- //
285
- // --- Disabling notifications ---
286
- // UI Config Build 3: Theme=light, Notifications=false
287
- }
302
+ const unsubscribe = myServiceWatcher.subscribe((resolvedArtifact) => {
303
+ if (resolvedArtifact.ready) {
304
+ console.log('myService updated:', resolvedArtifact.instance);
305
+ } else if (resolvedArtifact.error) {
306
+ console.error('myService error:', resolvedArtifact.error);
307
+ }
308
+ // The `get()` method can also be used inside the callback or outside
309
+ // to get the current state of the artifact.
310
+ const current = myServiceWatcher.get();
311
+ console.log('Current state from get():', current.instance);
312
+ });
288
313
 
289
- runReactiveStateExample();
314
+ // To stop receiving updates:
315
+ unsubscribe();
290
316
  ```
291
317
 
292
- ### Artifact-to-Artifact Dependencies
318
+ ### Reactive Dependencies (State Selection)
293
319
 
294
- Artifacts can depend on other artifacts using `ctx.use(({ resolve }) => resolve('otherArtifactKey'))` or `ctx.use(({ require }) => require('otherArtifactKey'))` within their factory. Changes (invalidations) to a dependency will automatically invalidate the dependent artifact.
320
+ Artifacts can react to changes in the global `DataStore` by using `ctx.select()`.
295
321
 
296
322
  ```typescript
297
- import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
298
- import { ReactiveDataStore } from '@asaidimu/utils-store';
299
-
300
- const store = new ReactiveDataStore({});
301
- const container = new ArtifactContainer(store);
323
+ interface UserSettings { userId: string; theme: string; };
324
+ type AppRegistry = { userPreference: string };
302
325
 
303
- let dbConnectionBuilds = 0;
304
- container.register({
305
- key: 'dbConnection',
306
- factory: async () => {
307
- dbConnectionBuilds++;
308
- console.log(`Establishing DB Connection (${dbConnectionBuilds})...`);
309
- await new Promise(res => setTimeout(res, 50)); // Simulate async work
310
- return { client: 'PostgreSQL', status: 'connected' };
311
- },
312
- scope: ArtifactScopes.Singleton,
313
- });
326
+ const userStore = new ReactiveDataStore<UserSettings>({ userId: 'guest', theme: 'light' });
327
+ const userContainer = new ArtifactContainer<AppRegistry, UserSettings>(userStore);
314
328
 
315
- let userRepositoryBuilds = 0;
316
- container.register({
317
- key: 'userRepository',
329
+ userContainer.register({
330
+ key: 'userPreference',
318
331
  factory: async ({ use }) => {
319
- userRepositoryBuilds++;
320
- console.log(`Building UserRepository (${userRepositoryBuilds})...`);
321
- const db = await use((ctx) => ctx.require('dbConnection')); // Using 'require' for direct instance
322
- return {
323
- dbClient: db.client,
324
- findUser: (id: string) => `User ${id} from ${db.client}`
325
- };
332
+ // This artifact will be rebuilt if state.theme changes
333
+ const theme = await use(({ select }) => select(state => state.theme));
334
+ return `Current theme is: ${theme}`;
326
335
  },
327
- scope: ArtifactScopes.Singleton,
328
336
  });
329
337
 
330
- async function runArtifactDependencyExample() {
331
- // Initial resolution of UserRepository, which will resolve dbConnection first
332
- let userRepo = await container.resolve('userRepository');
333
- console.assert(dbConnectionBuilds === 1, 'DB Connection built once');
334
- console.assert(userRepositoryBuilds === 1, 'User Repository built once');
335
- console.log('User Repo initialized:', userRepo.instance?.findUser('1'));
336
-
337
- // Manually invalidate the dbConnection. This will trigger a rebuild of both.
338
- console.log('\n--- Invalidating DB Connection ---');
339
- const dbConnection = await container.resolve('dbConnection');
340
- await dbConnection.invalidate(true); // `true` forces immediate rebuild
341
-
342
- // Resolve UserRepository again - it should have been rebuilt
343
- userRepo = await container.resolve('userRepository');
344
- console.assert(dbConnectionBuilds === 2, 'DB Connection rebuilt');
345
- console.assert(userRepositoryBuilds === 2, 'User Repository rebuilt due to DB change');
346
- console.log('User Repo after invalidation:', userRepo.instance?.findUser('2'));
347
-
348
- // Output:
349
- // Establishing DB Connection (1)...
350
- // Building UserRepository (1)...
351
- // User Repo initialized: User 1 from PostgreSQL
352
- //
353
- // --- Invalidating DB Connection ---
354
- // Establishing DB Connection (2)...
355
- // Building UserRepository (2)...
356
- // User Repo after invalidation: User 2 from PostgreSQL
338
+ async function runReactiveExample() {
339
+ let preference = await userContainer.resolve('userPreference');
340
+ console.log(preference.instance); // Output: Current theme is: light
341
+
342
+ // Update the store, which will trigger 'userPreference' to rebuild
343
+ await userStore.set({ theme: 'dark' });
344
+
345
+ // Resolve again to get the new instance
346
+ preference = await userContainer.resolve('userPreference');
347
+ console.log(preference.instance); // Output: Current theme is: dark
357
348
  }
358
349
 
359
- runArtifactDependencyExample();
350
+ runReactiveExample();
360
351
  ```
361
352
 
362
- ### Lifecycle Hooks: `onCleanup` & `onDispose`
363
-
364
- Artifacts can register cleanup functions to manage resources, ensuring proper release when instances are no longer needed.
353
+ ### Artifact Lifecycle (Cleanup & Dispose)
365
354
 
366
- * `onCleanup(callback)`: Registered callbacks are executed *before* a `Singleton` artifact's instance is replaced (due to invalidation/rebuild). Ideal for releasing resources tied to that specific *instance* (e.g., stopping timers, closing old connections).
367
- * `onDispose(callback)`: Registered callbacks are executed *only* when the artifact is explicitly `unregister()`ed from the container or the container itself is `dispose()`d. Ideal for final, permanent resource release associated with the *artifact definition itself*.
355
+ * `ctx.onCleanup(fn)`: Registers a function to run *before* a singleton artifact is rebuilt (due to invalidation) or before a transient artifact instance is discarded. Use this for instance-specific resource release (e.g., clearing timers, event listeners).
356
+ * `ctx.onDispose(fn)`: Registers a function to run *only when the artifact is permanently unregistered* from the container or the container itself is disposed. Use this for permanent resource release (e.g., closing database connections, unsubscribing from global events).
368
357
 
369
358
  ```typescript
370
- import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
371
- import { ReactiveDataStore } from '@asaidimu/utils-store';
372
-
373
- const store = new ReactiveDataStore({});
374
- const container = new ArtifactContainer(store);
375
-
376
- let currentInterval: any;
377
-
378
359
  container.register({
379
- key: 'ticker',
360
+ key: 'myResource',
361
+ scope: ArtifactScopes.Singleton,
380
362
  factory: ({ onCleanup, onDispose }) => {
381
- console.log('Ticker artifact created.');
382
- let count = 0;
383
- currentInterval = setInterval(() => {
384
- count++;
385
- // console.log(`Tick: ${count}`); // Uncomment to see live ticks
386
- }, 1000);
363
+ const resource = { id: Math.random(), intervalId: setInterval(() => {}, 1000) };
364
+ console.log(`Resource ${resource.id} created.`);
387
365
 
388
366
  onCleanup(() => {
389
- console.log('Cleaning up old Ticker instance (stopping interval)...');
390
- clearInterval(currentInterval);
367
+ console.log(`Cleaning up instance ${resource.id}...`);
368
+ clearInterval(resource.intervalId);
391
369
  });
392
370
 
393
371
  onDispose(() => {
394
- console.log('Disposing Ticker artifact (final cleanup)...');
395
- // Additional final cleanup could go here, e.g., closing file handles
372
+ console.log(`Disposing artifact 'myResource'.`);
373
+ // Additional permanent resource release here
396
374
  });
397
375
 
398
- return { start: Date.now() };
376
+ return resource;
399
377
  },
400
- scope: ArtifactScopes.Singleton,
401
- lazy: false, // Eagerly build
402
378
  });
403
379
 
404
- async function runLifecycleHooksExample() {
405
- await container.resolve('ticker');
406
- console.log('Ticker running for 2 seconds...');
407
- await new Promise(res => setTimeout(res, 2000));
408
-
409
- console.log('\n--- Invalidating Ticker (will trigger onCleanup and rebuild) ---');
410
- const ticker = await container.resolve('ticker'); // Get current to invalidate
411
- await ticker.invalidate(true); // Force rebuild
412
- console.log('Ticker rebuilt. Running for 2 more seconds...');
413
- await new Promise(res => setTimeout(res, 2000));
414
-
415
- console.log('\n--- Unregistering Ticker (will trigger onDispose) ---');
416
- await container.unregister('ticker');
417
- console.log('Ticker unregistered.');
380
+ async function lifecycleExample() {
381
+ await container.resolve('myResource');
382
+ // Simulate an invalidation (e.g., a dependency changed)
383
+ await container.invalidate('myResource'); // Triggers cleanup, then rebuilds
384
+ await container.resolve('myResource'); // A new instance is now resolved.
418
385
 
419
- // Output will show 'Cleaning up old Ticker instance' on invalidation,
420
- // and 'Disposing Ticker artifact' on unregister.
386
+ // When unregistering, onDispose is called
387
+ await container.unregister('myResource'); // Triggers onCleanup (if active), then onDispose
421
388
  }
422
-
423
- runLifecycleHooksExample();
389
+ lifecycleExample();
424
390
  ```
425
391
 
426
- ### Watching Artifacts for Changes
392
+ ### Streaming Artifacts
427
393
 
428
- The `container.watch(key)` 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 where you want to be notified of changes as they occur.
394
+ Singletons can continuously emit new values using `ctx.stream()`. This is powerful for reactive data sources.
429
395
 
430
396
  ```typescript
431
- import { ArtifactContainer, ArtifactScopes, WatcherDisposedError } from '@asaidimu/utils-artifacts';
432
- import { ReactiveDataStore } from '@asaidimu/utils-store';
397
+ container.register({
398
+ key: 'counterStream',
399
+ scope: ArtifactScopes.Singleton,
400
+ factory: ({ stream, onCleanup }) => {
401
+ let count = 0;
402
+ let interval: ReturnType<typeof setInterval>;
433
403
 
434
- interface AppState {
435
- counter: number;
436
- }
404
+ stream(async ({ emit, signal }) => {
405
+ console.log('Counter stream started...');
406
+ interval = setInterval(() => {
407
+ if (signal.aborted) {
408
+ console.log('Stream aborted, stopping interval.');
409
+ clearInterval(interval);
410
+ return;
411
+ }
412
+ count++;
413
+ emit(count); // Emit the new value
414
+ if (count >= 5) {
415
+ console.log('Count limit reached, stopping stream.');
416
+ clearInterval(interval);
417
+ return; // Stream producer can return to end the stream
418
+ }
419
+ }, 500);
420
+ });
437
421
 
438
- const store = new ReactiveDataStore<AppState>({ counter: 0 });
439
- const container = new ArtifactContainer<any, AppState>(store);
422
+ onCleanup(() => {
423
+ console.log('Cleaning up counter stream instance...');
424
+ // Ensure interval is cleared if stream is aborted/rebuilt
425
+ clearInterval(interval);
426
+ });
440
427
 
441
- let reactiveCounterBuilds = 0;
442
- container.register({
443
- key: 'reactiveCounter',
444
- factory: async ({ use }) => {
445
- reactiveCounterBuilds++;
446
- const count = await use((ctx) => ctx.select((state) => state.counter));
447
- return `Current count: ${count} (Build: ${reactiveCounterBuilds})`;
428
+ return 0; // Initial value before stream starts emitting
448
429
  },
449
- scope: ArtifactScopes.Singleton,
450
430
  });
451
431
 
452
- async function runWatcherExample() {
453
- console.log('Setting up watcher...');
454
- const watcher = container.watch('reactiveCounter');
455
-
456
- let unsubscribe = watcher.subscribe((resolved) => {
457
- if (resolved.ready) {
458
- console.log('Watcher received update:', resolved.instance);
459
- } else if (resolved.error) {
460
- console.error('Watcher received error:', resolved.error.message);
461
- } else {
462
- console.log('Watcher received pending update (artifact not yet ready).');
463
- }
432
+ async function streamExample() {
433
+ const watcher = container.watch('counterStream');
434
+ const unsubscribe = watcher.subscribe((art) => {
435
+ if (art.ready) console.log('Counter value:', art.instance);
464
436
  });
465
437
 
466
- // Give watcher time to resolve the artifact initially
467
- await new Promise(res => setTimeout(res, 10));
468
-
469
- console.log('\n--- Incrementing counter ---');
470
- await store.set({ counter: 1 });
471
- await new Promise(res => setTimeout(res, 10));
472
-
473
- console.log('\n--- Incrementing counter again ---');
474
- await store.set({ counter: 2 });
475
- await new Promise(res => setTimeout(res, 10));
476
-
477
- console.log('\n--- Unsubscribing & Disposing watcher ---');
478
- unsubscribe(); // Unsubscribe from updates
479
-
480
- // After all subscribers are gone, the underlying artifact instance for watching transients is cleaned up.
481
- // The watcher itself can then be disposed, making it unusable.
482
- await new Promise(res => setTimeout(res, 10)); // Give cleanup a moment
483
- // For singletons, watcher.dispose() is effectively a no-op as the underlying artifact remains
484
- // For transients, it would clean up the internal singleton clone.
485
-
486
- try {
487
- watcher.get(); // Attempt to access after dispose
488
- } catch (e) {
489
- if (e instanceof WatcherDisposedError) {
490
- console.error('Caught expected error after watcher dispose:', e.message);
491
- } else {
492
- throw e;
493
- }
494
- }
495
-
496
- // Output:
497
- // Setting up watcher...
498
- // Watcher received pending update (artifact not yet ready).
499
- // Watcher received update: Current count: 0 (Build: 1)
500
- //
501
- // --- Incrementing counter ---
502
- // Watcher received update: Current count: 1 (Build: 2)
503
- //
504
- // --- Incrementing counter again ---
505
- // Watcher received update: Current count: 2 (Build: 3)
506
- //
507
- // --- Unsubscribing & Disposing watcher ---
508
- // Caught expected error after watcher dispose: [ArtifactContainer] Artifact with key:reactiveCounter has already been disposed
438
+ // Keep alive for a few seconds to see stream emissions
439
+ await new Promise(res => setTimeout(res, 3000));
440
+ unsubscribe();
441
+ console.log('Stream watcher unsubscribed.');
509
442
  }
510
-
511
- runWatcherExample();
443
+ streamExample();
512
444
  ```
513
445
 
514
- ### Streaming Artifacts with `ctx.stream()`
446
+ ### Invalidating Artifacts
515
447
 
516
- `Singleton` artifacts can leverage `ctx.stream(callback)` to become continuous sources of data. The `callback` receives an `ArtifactStreamContext` which allows emitting new values over time via `ctx.emit(value)`. Each emission updates the artifact's instance, notifying all dependents and watchers immediately. This is ideal for scenarios like WebSocket clients, background workers, or long-running computations that produce intermediate results.
448
+ You can manually trigger an artifact to rebuild, which will also cascade invalidations to its dependents.
517
449
 
518
450
  ```typescript
519
- import { ArtifactContainer, ArtifactScopes } from "@asaidimu/utils-artifacts";
520
- import { ReactiveDataStore } from "@asaidimu/utils-store";
521
-
522
- const store = new ReactiveDataStore({});
523
- const container = new ArtifactContainer<{ producer: number; consumer: string }, {}>(store);
451
+ // Invalidate a specific artifact
452
+ await container.invalidate('myArtifact');
524
453
 
525
- const PRODUCER_TIMEOUT = 50; // ms
526
- const PRODUCER_LIMIT = 3;
454
+ // Force immediate rebuild, bypassing any debounce delay
455
+ await container.invalidate('myArtifact', true);
456
+ ```
527
457
 
528
- container.register({
529
- key: "producer",
530
- factory: ({ stream }) => {
531
- let value = 0;
458
+ ### Debugging Artifacts
532
459
 
533
- // The stream callback is executed in the background
534
- stream(async ({ emit, signal }) => {
535
- console.log("Producer: Stream started.");
536
- while (!signal.aborted) {
537
- await new Promise((res) => setTimeout(res, PRODUCER_TIMEOUT)); // Simulate async work
538
- value++;
539
- console.log(`Producer: Emitting value: ${value}`);
540
- await emit(value); // Emit new value, notifying dependents and watchers
541
- if (value >= PRODUCER_LIMIT) {
542
- console.log("Producer: Reached limit, stopping stream.");
543
- break;
544
- }
545
- }
546
- });
460
+ The `debugInfo()` method provides a snapshot of the container's internal state, useful for understanding dependencies, status, and build counts.
547
461
 
548
- return value; // Initial value returned by the factory
549
- },
550
- scope: ArtifactScopes.Singleton,
462
+ ```typescript
463
+ const debugNodes = container.debugInfo();
464
+ debugNodes.forEach(node => {
465
+ console.log(`\nID: ${node.id}`);
466
+ console.log(` Scope: ${node.scope}`);
467
+ console.log(` Status: ${node.status}`); // active, error, idle, building, pending, debouncing
468
+ console.log(` Dependencies (Artifacts): ${node.dependencies.join(', ') || 'None'}`);
469
+ console.log(` Dependencies (State Paths): ${node.stateDependencies.join(', ') || 'None'}`);
470
+ console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
471
+ console.log(` Build Count: ${node.buildCount}`);
551
472
  });
473
+ ```
552
474
 
553
- container.register({
554
- key: "consumer",
555
- factory: async ({ use }) => {
556
- // This artifact depends on the 'producer'
557
- const producerValue = await use(({ require }) => require("producer"));
558
- return `Consumed: ${producerValue}`;
559
- },
560
- scope: ArtifactScopes.Singleton,
561
- });
475
+ ### Retry Utility
562
476
 
563
- async function runStreamExample() {
564
- const watcher = container.watch("consumer");
565
- const receivedValues: string[] = [];
477
+ The `Retry` class provides flexible retry logic for any async operation, supporting various strategies.
566
478
 
567
- const unsubscribe = watcher.subscribe((resolved) => {
568
- if (resolved.instance && !receivedValues.includes(resolved.instance)) {
569
- receivedValues.push(resolved.instance);
570
- console.log(`Watcher: Received "${resolved.instance}"`);
571
- }
572
- });
479
+ ```typescript
480
+ import { Retry, RetryExhaustedError, RetryPredicates } from '@asaidimu/utils-artifacts/retry';
573
481
 
574
- // Initial resolve to kick-start the producer factory and stream
575
- await container.resolve("consumer");
482
+ const unreliableOperation = async (attempt: number) => {
483
+ if (attempt < 3) {
484
+ console.log(`Unreliable operation failing on attempt ${attempt}`);
485
+ throw new Error('Transient network error');
486
+ }
487
+ console.log(`Unreliable operation succeeding on attempt ${attempt}`);
488
+ return 'Success!';
489
+ };
576
490
 
577
- // Give enough time for the stream to run and emit all values
578
- // (PRODUCER_LIMIT * PRODUCER_TIMEOUT) + some buffer for processing
579
- await new Promise((res) => setTimeout(res, (PRODUCER_LIMIT * PRODUCER_TIMEOUT) + 100));
491
+ async function runRetryExample() {
492
+ try {
493
+ const result = await Retry.execute(
494
+ () => unreliableOperation(retryAttempt), // `retryAttempt` is for demonstration, actual attempt is internal.
495
+ {
496
+ retries: 4, // Total attempts: 1 (initial) + 4 (retries) = 5
497
+ strategy: 'exponential',
498
+ delay: 100, // Base delay 100ms
499
+ factor: 2, // Multiplier 2x (100, 200, 400, 800)
500
+ maxDelay: 1000,
501
+ onRetry: (err, attempt, nextDelay) => {
502
+ console.warn(`Attempt ${attempt} failed: ${err}. Retrying in ${nextDelay}ms.`);
503
+ retryAttempt = attempt; // For demonstration only
504
+ },
505
+ }
506
+ );
507
+ console.log('Retry successful:', result);
508
+ } catch (e) {
509
+ if (e instanceof RetryExhaustedError) {
510
+ console.error(`Retry exhausted after ${e.attempts} attempts. Last error:`, e.lastError);
511
+ } else {
512
+ console.error('Unexpected error:', e);
513
+ }
514
+ }
580
515
 
581
- console.log('\n--- Stream Complete ---');
582
- console.log('All received values:', receivedValues);
583
- // Expected: ["Consumed: 0", "Consumed: 1", "Consumed: 2", "Consumed: 3"]
516
+ // Example with conditional retry based on error type
517
+ const fetchWithRetry = async (url: string) => {
518
+ return Retry.execute(
519
+ async () => {
520
+ const response = await fetch(url);
521
+ if (response.status >= 500) {
522
+ throw { status: response.status, message: 'Server error' }; // Simulate HTTP 5xx
523
+ }
524
+ return response.json();
525
+ },
526
+ {
527
+ retries: 5,
528
+ strategy: 'conditional',
529
+ shouldRetry: RetryPredicates.any(
530
+ RetryPredicates.networkErrors,
531
+ RetryPredicates.serverErrors,
532
+ RetryPredicates.httpStatus(429) // Also retry on Too Many Requests
533
+ ),
534
+ delay: (attempt) => Math.min(100 * Math.pow(2, attempt), 2000), // Custom delay function
535
+ }
536
+ );
537
+ };
584
538
 
585
- unsubscribe(); // Clean up the watcher
539
+ // const data = await fetchWithRetry('https://api.example.com/data');
586
540
  }
587
-
588
- runStreamExample();
541
+ let retryAttempt = 0; // Used for demonstration purposes only
542
+ runRetryExample();
589
543
  ```
590
544
 
591
- ### Debugging Information
545
+ ### Concurrency Utilities (Once & Serializer)
592
546
 
593
- Use `container.debugInfo()` to inspect the current state of all registered artifacts within a container, including their status, direct dependencies (artifact-to-artifact and state), and dependents. This is an invaluable tool for understanding your application's dependency graph at runtime.
547
+ `Once` ensures a function runs exactly one time, caching its result. `Serializer` ensures functions run sequentially. These are primarily used internally but are exposed for advanced use cases.
594
548
 
595
549
  ```typescript
596
- import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
597
- import { ReactiveDataStore } from '@asaidimu/utils-store';
598
-
599
- interface AppState {
600
- configVersion: number;
550
+ import { Once, Serializer } from '@asaidimu/utils-artifacts/sync';
551
+
552
+ async function runOnceExample() {
553
+ const initialization = new Once<string>();
554
+ const expensiveInit = async () => {
555
+ console.log('Performing expensive initialization...');
556
+ await new Promise(res => setTimeout(res, 200));
557
+ return 'Initialized Resource';
558
+ };
559
+
560
+ const [res1, res2, res3] = await Promise.all([
561
+ initialization.do(expensiveInit),
562
+ initialization.do(expensiveInit),
563
+ initialization.do(expensiveInit),
564
+ ]);
565
+
566
+ console.log(res1.value, res2.value, res3.value); // All will be 'Initialized Resource'
567
+ // expensiveInit will be called only once.
601
568
  }
602
-
603
- const store = new ReactiveDataStore<AppState>({ configVersion: 1 });
604
- const container = new ArtifactContainer<any, AppState>(store);
605
-
606
- container.register({
607
- key: 'configService',
608
- factory: async ({ use }) => {
609
- const version = await use((ctx) => ctx.select((state) => state.configVersion));
610
- return `Config v${version}`;
611
- },
612
- scope: ArtifactScopes.Singleton,
613
- });
614
-
615
- container.register({
616
- key: 'dataProcessor',
617
- factory: async ({ use }) => {
618
- const config = await use((ctx) => ctx.resolve('configService'));
619
- return `Processing with ${config.instance}`;
620
- },
621
- scope: ArtifactScopes.Singleton,
622
- });
623
-
624
- container.register({
625
- key: 'ephemeralTool',
626
- factory: () => 'A transient tool',
627
- scope: ArtifactScopes.Transient,
628
- });
629
-
630
- async function runDebugInfoExample() {
631
- await container.resolve('dataProcessor'); // Resolve to build dependencies
632
- await container.resolve('ephemeralTool'); // Resolve a transient (doesn't cache, but will show up if just resolved)
633
-
634
- const debugInfo = container.debugInfo();
635
- console.log('--- Artifact Debug Information ---');
636
- debugInfo.forEach(node => {
637
- console.log(`\nID: ${node.id}`);
638
- console.log(` Scope: ${node.scope}`);
639
- console.log(` Status: ${node.status}`);
640
- console.log(` Dependencies (Artifacts): ${node.dependencies.join(', ') || 'None'}`);
641
- console.log(` Dependencies (State Paths): ${node.stateDependencies.join(', ') || 'None'}`);
642
- console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
643
- console.log(` Build Count: ${node.renderCount}`);
644
- console.log('---');
645
- });
646
-
647
- // Example output for 'dataProcessor':
648
- // ID: dataProcessor
649
- // Scope: singleton
650
- // Status: active
651
- // Dependencies (Artifacts): configService
652
- // Dependencies (State Paths): None
653
- // Dependents: None
654
- // Build Count: 1
569
+ runOnceExample();
570
+
571
+ async function runSerializerExample() {
572
+ const queue = new Serializer<string>();
573
+ const order: string[] = [];
574
+
575
+ const task1 = async () => {
576
+ await new Promise(res => setTimeout(res, 100));
577
+ order.push('Task 1');
578
+ return 'Result 1';
579
+ };
580
+ const task2 = async () => {
581
+ order.push('Task 2');
582
+ return 'Result 2';
583
+ };
584
+ const task3 = async () => {
585
+ await new Promise(res => setTimeout(res, 50));
586
+ order.push('Task 3');
587
+ return 'Result 3';
588
+ };
589
+
590
+ await Promise.all([
591
+ queue.do(task1),
592
+ queue.do(task2),
593
+ queue.do(task3),
594
+ ]);
595
+
596
+ console.log(order); // Expected: ['Task 1', 'Task 2', 'Task 3']
655
597
  }
656
-
657
- runDebugInfoExample();
598
+ runSerializerExample();
658
599
  ```
659
600
 
660
601
  ---
661
602
 
662
- ## 🏛️ Project Architecture
663
-
664
- 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). It is designed to be modular and extensible, promoting maintainability and testability.
603
+ ## 🏗️ Project Architecture
665
604
 
666
- ### Core Components
605
+ The `ArtifactContainer` is designed with a modular architecture, delegating responsibilities to specialized internal components:
667
606
 
668
- * **`ArtifactContainer`**: The public-facing entry point of the system. It orchestrates the registration, resolution, lifecycle management (`Singleton`/`Transient`), and reactive updates of all artifacts. It provides methods like `register`, `resolve`, `watch`, `invalidate`, and `debugInfo`.
669
- * **`ArtifactRegistry`**: Manages the definitions of all registered artifacts, storing their factory functions and configuration options (scope, lazy loading, retries, debounce, timeout). It ensures unique keys and provides basic CRUD for artifact templates.
670
- * **`ArtifactCache`**: Stores the runtime state of resolved `Singleton` artifacts, including their instances, errors, cleanup functions, state dependencies, and internal synchronization primitives (`Once`, `Serializer`). It's responsible for packaging cached data into the public `ResolvedArtifact` format, maintaining reference stability.
671
- * **`ArtifactDependencyGraph`**: A specialized wrapper around a generic `DependencyGraph` that tracks artifact-to-artifact dependencies and state path dependencies. It's crucial for detecting circular dependencies, managing the dependency graph, and determining the cascade order during invalidation.
672
- * **`ArtifactManager`**: The core engine responsible for the actual building, invalidation, and disposal of artifact instances. It interacts heavily with the `Registry`, `Cache`, `DependencyGraph`, and `DataStore`. It implements retry logic, timeouts, and manages lifecycle hooks (`onCleanup`, `onDispose`) and `ctx.stream` functionality.
673
- * **`ArtifactObserverManager`**: Handles the creation and management of `ArtifactObserver` instances, allowing external code to subscribe to artifact changes. It also manages reference counting for watchers and notifies them of updates, including special handling for `Transient` artifacts to ensure they are watched via a temporary `Singleton` clone.
674
- * **`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 to state changes), `get` (for reading the current state), and `set` (allowing artifacts to dispatch state updates).
675
- * **Synchronization Primitives (`Mutex`, `Once`, `Serializer`)**: These internal utilities (`src/artifacts/sync.ts`) provide robust mechanisms for managing concurrency, ensuring single execution of tasks, and processing operations sequentially. They are fundamental building blocks for the `ArtifactManager` and `ArtifactCache`.
676
- * **Error Handling (`ArtifactError` hierarchy)**: A dedicated set of custom error classes (`src/artifacts/errors.ts`) provides clear, categorized error messages for system-level issues (e.g., circular dependencies, missing artifacts, illegal operations), simplifying debugging and error handling.
607
+ * **`ArtifactContainer`**: The public API entry point. It orchestrates interactions between the other internal components, providing a unified interface for artifact management.
608
+ * **`ArtifactRegistry`**: Stores the definitions (`ArtifactTemplate`s) of all registered artifacts, mapping unique keys to their factory functions and configuration options.
609
+ * **`ArtifactCache`**: Manages the storage and retrieval of resolved artifact instances, particularly for `Singleton` scoped artifacts. It handles caching, versioning, and state associated with active instances.
610
+ * **`ArtifactDependencyGraph`**: A bidirectional graph (`DependencyGraph`) that maps artifact-to-artifact dependencies and tracks which artifacts depend on state paths. This is crucial for circular dependency detection and efficient invalidation cascades.
611
+ * **`ArtifactManager`**: The core lifecycle manager. It handles the intricate process of building artifacts (executing factories), managing retries and timeouts, orchestrating `onCleanup`/`onDispose`, propagating stream emissions, and managing the reactive invalidation process based on artifact and state dependencies.
612
+ * **`ArtifactObserverManager`**: Manages the `container.watch()` API, maintaining subscriptions from external consumers and notifying them of artifact state changes. It handles reference counting and lazy initialization of watched artifacts.
613
+ * **`Retry`**: A standalone utility providing flexible retry mechanisms (fixed, exponential, jittered, conditional) for any asynchronous operation.
614
+ * **`Once` & `Serializer`**: Low-level concurrency primitives used internally (and exposed) to ensure that asynchronous operations (like artifact builds or stream emissions) execute safely and predictably, avoiding race conditions.
677
615
 
678
616
  ### Data Flow
679
617
 
680
- 1. **Registration (`container.register`)**: An `ArtifactTemplate` (containing an `ArtifactFactory` and its `options`) is stored in the `ArtifactRegistry`. A corresponding node is added to the `ArtifactDependencyGraph`.
681
- 2. **Resolution (`container.resolve` or `container.require`)**:
682
- * If the artifact is a `Singleton` and already built, its cached instance from `ArtifactCache` is returned.
683
- * If not built (or `Transient`), the `ArtifactManager` invokes the `ArtifactFactory` within a specialized `ArtifactFactoryContext`.
684
- * **Dependency Declaration (`ctx.use`)**: Inside the factory, `ctx.use` provides methods to declare dependencies:
685
- * `ctx.resolve(key)`/`ctx.require(key)`: Recursively triggers resolution of another artifact. The relationship is added to the `ArtifactDependencyGraph`.
686
- * `ctx.select(selector)`: Extracts specific state paths from the global `DataStore`. These paths are registered as state dependencies for the artifact.
687
- 3. **Dependency Graph Construction**: As dependencies are declared (via `ctx.resolve`/`ctx.require` or `ctx.select`), the `ArtifactDependencyGraph` constructs a precise, bidirectional graph, linking artifacts and their state dependencies.
688
- 4. **Reactive Invalidation**:
689
- * **State Changes**: When a `DataStore` value changes, `ArtifactManager` identifies affected `Singleton` artifacts (those subscribed to the changed state paths) and calls `container.invalidate()` for them.
690
- * **Artifact Changes**: When a `Singleton` artifact is rebuilt, explicitly `invalidate()`d, or proactively `stream()`s a new value, its direct and transitive `dependents` are identified by the `ArtifactDependencyGraph`, and `container.invalidate()` is called for each, cascading updates.
691
- 5. **Rebuild Process**: `container.invalidate()` triggers:
692
- * **Debouncing**: If configured (`debounce` option), rebuilds are debounced (`activeDebounceMs`) to consolidate multiple rapid invalidations, preventing excessive factory executions.
693
- * **Cleanup**: `onCleanup` hooks for the *old* instance are executed.
694
- * **New Instance**: The `ArtifactFactory` is re-executed by the `ArtifactManager`, creating a new instance.
695
- * **Graph Update**: The dependency graph is updated based on any *new* dependencies declared by the re-executed factory.
696
- * **Notification**: `ArtifactObserver` subscribers are notified of the new instance via `ArtifactObserverManager`.
697
- 6. **Streaming Updates (`ctx.stream`)**: If a `Singleton` artifact uses `ctx.stream`, subsequent `ctx.emit(value)` calls from within its factory:
698
- * Update the artifact's cached instance in `ArtifactCache`.
699
- * Increment the artifact's internal version.
700
- * Trigger `invalidate()` on all its dependents and `notifyObservers()`, effectively propagating the streamed value reactively.
701
- 7. **Disposal (`container.dispose` / `container.unregister`)**: All resources associated with an artifact (`onDispose` hooks, state subscriptions, cache entry, graph links) are released. Full container disposal clears everything.
618
+ 1. **Registration**: An `ArtifactTemplate` is registered with the `ArtifactRegistry`. A corresponding node is added to the `ArtifactDependencyGraph`.
619
+ 2. **Resolution (`container.resolve`)**:
620
+ * The `ArtifactContainer` forwards the request to the `ArtifactManager`.
621
+ * The `Manager` consults the `ArtifactCache`. If a `Singleton` is already built and fresh, it's returned immediately.
622
+ * Otherwise, the `Manager` retrieves the `ArtifactTemplate` from the `Registry`.
623
+ * A factory is executed with an `ArtifactFactoryContext`.
624
+ * Inside the factory:
625
+ * `ctx.use(({ resolve }) => ...)`: Triggers recursive resolution of dependent artifacts. The `Manager` performs cycle detection via the `DependencyGraph` and updates artifact dependencies.
626
+ * `ctx.use(({ select }) => ...)`: Registers state path dependencies with the `Manager`, which then subscribes to these paths via the `DataStore`.
627
+ * `ctx.stream(...)`: For singletons, registers a stream producer that can `emit` new values.
628
+ * `ctx.onCleanup`/`onDispose`: Registers lifecycle hooks.
629
+ * The `Manager` handles retries for factory execution and commits the resulting instance (or error) to the `ArtifactCache`.
630
+ * The `ArtifactCache` packages the result into a `ResolvedArtifact` for the consumer.
631
+ 3. **Invalidation**:
632
+ * **State Change**: A change in the `DataStore` (observed by `ArtifactManager` via `store.watch()`) triggers invalidation of dependent artifacts.
633
+ * **Artifact Change**: A dependency being rebuilt or a stream emitting a new value, or manual `container.invalidate()` triggers invalidation.
634
+ * The `ArtifactManager` runs the `onCleanup` hooks for the old instance, removes it from the `ArtifactCache`, and uses the `DependencyGraph` to identify and cascade invalidations to all affected dependents.
635
+ * If configured (e.g., not lazy, has watchers), the `Manager` triggers a rebuild for the artifact.
636
+ * Finally, `ArtifactObserverManager` notifies all active watchers.
702
637
 
703
638
  ### Extension Points
704
639
 
705
- The primary extension point is the **`ArtifactFactory` function**. By implementing custom factories, you can:
706
- * Integrate with any third-party library or framework (e.g., database clients, API wrappers, authentication services).
707
- * Define complex business logic that reacts to state or other services.
708
- * Manage external resources like database connections, API clients, or message queues, ensuring they are properly initialized and cleaned up using `onCleanup` and `onDispose`.
709
- * Provide custom logging, monitoring, or telemetry by injecting those services as dependencies.
640
+ The primary extension point is the `ArtifactFactory` function itself, which receives the `ArtifactFactoryContext`. This context allows artifacts to:
710
641
 
711
- The `ArtifactContainer` itself is designed to be highly configurable via `ArtifactTemplate` options, allowing fine-tuning of scope, lazy loading, error handling, and reactive behavior.
642
+ * Declare and react to external dependencies (other artifacts, global state).
643
+ * Manage their internal lifecycle (cleanup, disposal).
644
+ * Create streaming data sources.
645
+ * Integrate with the underlying `DataStore` for dispatching actions.
712
646
 
713
647
  ---
714
648
 
715
- ## 👨‍💻 Development & Contributing
716
-
717
- We welcome contributions! Please read the guidelines below.
649
+ ## 🛠️ Development & Contributing
718
650
 
719
651
  ### Development Setup
720
652
 
721
- 1. **Clone the repository**:
653
+ To set up the project for local development:
654
+
655
+ 1. **Clone the repository:**
722
656
  ```bash
723
657
  git clone https://github.com/asaidimu/erp-utils.git
724
- cd erp-utils/src/artifacts # Navigate to this package's directory
658
+ cd erp-utils/src/artifacts
725
659
  ```
726
- 2. **Install dependencies**:
660
+ 2. **Install dependencies:**
727
661
  ```bash
728
- bun install
729
- # or npm install
730
- # or yarn install
662
+ npm install
663
+ # or
664
+ yarn install
731
665
  ```
732
- 3. **Build (if necessary for changes outside `src`)**:
666
+ 3. **Build the project (if applicable, though typically handled by IDE/watch mode):**
733
667
  ```bash
734
- bun run build
668
+ npm run build # Or `tsc` if not defined in package.json scripts
735
669
  ```
736
670
 
737
671
  ### Scripts
738
672
 
739
- The `package.json` for this package includes the following scripts:
673
+ The `package.json` defines the following scripts:
740
674
 
741
- * `bun install`: Installs project dependencies.
742
- * `bun run test`: Runs the test suite using Vitest.
743
- * `bun run test:watch`: Runs tests in watch mode for continuous feedback.
744
- * `bun run test:browser`: Runs tests in a browser environment using Playwright.
745
- * `bun run build`: Compiles the TypeScript source code into JavaScript.
675
+ * `npm test`: Runs all tests once.
676
+ * `npm test:watch`: Runs tests in watch mode, rerunning on file changes.
677
+ * `npm test:browser`: Runs tests in a browser environment (if configured), typically once.
746
678
 
747
679
  ### Testing
748
680
 
749
- This project uses [Vitest](https://vitest.dev/) for testing, with support for browser-based tests via Playwright.
681
+ The project uses `vitest` for testing.
750
682
 
751
- * To run all tests:
752
- ```bash
753
- bun run test
754
- ```
755
- * To run tests in watch mode:
756
- ```bash
757
- bun run test --watch
758
- ```
759
- * To run tests in a browser environment:
760
- ```bash
761
- bun run test:browser
762
- ```
763
- * Ensure all new features or bug fixes are accompanied by appropriate unit and/or integration tests.
683
+ * To run all tests: `npm test`
684
+ * To run tests continuously during development: `npm test:watch`
685
+
686
+ Tests utilize `fake-indexeddb` as seen in `vitest.setup.ts`, ensuring a consistent environment.
764
687
 
765
688
  ### Contributing Guidelines
766
689
 
767
- We appreciate your interest in contributing to `@asaidimu/utils-artifacts`!
768
- Please refer to the main repository's [CONTRIBUTING.md](https://github.com/asaidimu/erp-utils/blob/main/CONTRIBUTING.md) for detailed guidelines on:
690
+ We welcome contributions! Please follow these guidelines:
769
691
 
770
- * Reporting issues
771
- * Submitting pull requests
772
- * Coding standards
773
- * Commit message conventions (we follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/))
692
+ 1. **Fork the repository** and create your branch from `main`.
693
+ 2. **Ensure code quality**: Write clean, readable TypeScript code. Adhere to existing coding style (ESLint and Prettier are typically configured in parent project).
694
+ 3. **Tests**: All new features and bug fixes should be accompanied by appropriate unit or integration tests. Ensure existing tests pass.
695
+ 4. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history. (e.g., `feat: add new artifact scope`, `fix: resolve circular dependency issue`).
696
+ 5. **Pull Requests**:
697
+ * Open a detailed Pull Request describing the changes, new features, or bug fixes.
698
+ * Reference any related issues.
699
+ * Ensure your branch is up-to-date with `main`.
774
700
 
775
701
  ### Issue Reporting
776
702
 
777
- Found a bug or have a feature request? Please open an issue on our [GitHub Issues page](https://github.com/asaidimu/erp-utils/issues).
778
- Provide as much detail as possible, including steps to reproduce, expected behavior, and your environment.
703
+ If you find a bug or have a feature request, please open an issue on the [GitHub Issues page](https://github.com/asaidimu/erp-utils/issues). Provide as much detail as possible, including steps to reproduce bugs and clear descriptions for feature requests.
779
704
 
780
705
  ---
781
706
 
782
- ## Additional Information
707
+ ## ℹ️ Additional Information
783
708
 
784
709
  ### Troubleshooting
785
710
 
786
- * **`ArtifactNotFoundError`**: This typically means you're trying to `resolve` or `watch` an artifact that hasn't been `register`ed in the current container. Double-check your `key`s and registration calls.
787
- * **`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.
788
- * **Artifact Not Updating Reactively**:
789
- * Ensure your artifact is a `Singleton` (Transient artifacts do not cache, react to invalidations, or support streaming).
790
- * Verify you are using `ctx.use()` and `ctx.select()` (for state dependencies) or `ctx.use(({ resolve }) => resolve('otherKey'))` (for artifact dependencies) inside your factory to properly declare dependencies.
791
- * Check that your `DataStore` implementation is correctly triggering its `watch` callbacks when state changes.
792
- * **`IllegalScopeError`**: You might be trying to `stream` a value from a `Transient` artifact. The `stream` method is only meaningful for `Singleton` artifacts, which maintain a persistent, updateable instance.
793
- * **`TimeoutError`**: Your artifact factory took longer than its configured `timeout` to complete, or an internal `Mutex` lock operation exceeded its timeout. Consider increasing the `timeout` in the artifact's `ArtifactTemplate` or optimizing your factory's logic.
794
- * **`WatcherDisposedError`**: You are attempting to access a watcher's state (`watcher.get()`) after `watcher.dispose()` has been called. Ensure you unsubscribe from all callbacks (`unsubscribe()`) and dispose of watchers when no longer needed (`watcher.dispose()`), and avoid using them afterward.
711
+ * **`ArtifactNotFoundError`**: This means you're trying to `resolve` or `watch` an artifact that hasn't been `register`ed. Double-check your artifact keys and ensure registration happens before resolution.
712
+ * **`CircularDependencyError`**: This occurs when your artifact graph forms a loop (e.g., A depends on B, B depends on A). The `debugInfo()` output and the error message's path can help you identify the cycle. Redesign your dependencies to break the cycle.
713
+ * **`TimeoutError`**: Your artifact factory took longer than the specified `timeout` during registration. This can indicate long-running sync operations, slow async dependencies, or an infinite loop. Increase the timeout or optimize your factory.
714
+ * **Unexpected Rebuilds/No Rebuilds**:
715
+ * Use `container.debugInfo()` to inspect artifact statuses and `stateDependencies`/`dependencies`.
716
+ * Ensure your `ctx.select()` selectors correctly identify the state slices you intend to react to.
717
+ * Check `debounce` settings if an artifact seems to rebuild too frequently or too slowly.
718
+ * Verify `onCleanup` and `onDispose` hooks are being called as expected to rule out resource leaks.
795
719
 
796
720
  ### FAQ
797
721
 
798
- * **What is an "artifact" in this context?**
799
- 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.
800
- * **Why use this over a simpler DI library?**
801
- `@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.
802
- * **When should I use `Singleton` vs. `Transient`?**
803
- * **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, and they react to dependency changes.
804
- * **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, short-lived outputs. They do not react to dependencies or retain state.
805
- * **How does `debounce` work?**
806
- The `debounce` option (part of `ArtifactTemplate`) prevents a `Singleton` 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 (or after the last invalidation within the debounce window). This is crucial for performance with rapid state changes, especially when many state changes might trigger the same artifact rebuild.
722
+ * **What's the difference between `Singleton` and `Transient` scopes?**
723
+ * `Singleton`: Only one instance of the artifact is ever created. Subsequent `resolve` calls return the same instance. This is suitable for services, configurations, or shared resources. They support state dependencies, `onCleanup`/`onDispose`, and `stream`.
724
+ * `Transient`: A new instance is created every time the artifact is resolved. Useful for ephemeral objects that should not be shared or cached. They do not support `stream` or persistent `onCleanup`/`onDispose` (though a single `cleanup` can be returned by `resolve`).
725
+ * **When should I use `resolve` versus `require`?**
726
+ * Use `resolve` when you need to defensively handle potential errors or pending states of an artifact. It returns a `ResolvedArtifact` object that allows explicit checks (`.ready`, `.error`).
727
+ * Use `require` when you are confident the artifact will resolve successfully and prefer to get the instance directly, allowing errors to propagate as exceptions. This simplifies code where error handling is done at a higher level.
728
+ * **How does `debounce` work for invalidation?**
729
+ `debounce` adds a delay (in milliseconds) before an artifact rebuilds after its dependencies change. If multiple dependency changes occur within this debounce period, the rebuild is aggregated into a single event, preventing excessive rapid rebuilds. `container.invalidate(key, true)` can bypass this debounce.
730
+ * **What is the purpose of `onCleanup` vs. `onDispose`?**
731
+ * `onCleanup`: Tied to the *current instance's lifecycle*. It runs when a `Singleton` artifact's instance is replaced (e.g., due to an invalidation and rebuild), or when a `Transient` instance is returned and subsequently discarded. Use for resources specific to that particular instance.
732
+ * `onDispose`: Tied to the *artifact's registration lifecycle*. It runs only when the artifact is permanently `unregister`ed from the container or when the container itself is `dispose`d. Use for resources that should persist across instance rebuilds but be released when the artifact itself is no longer managed.
807
733
 
808
- ### Changelog / Roadmap
734
+ ### Changelog/Roadmap
809
735
 
810
- * **Changelog**: For a detailed list of changes, please refer to the [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/CHANGELOG.md) file in the main repository.
811
- * **Roadmap**: Future plans and upcoming features for `@asaidimu/utils-artifacts` are typically outlined in the main repository's [issue tracker](https://github.com/asaidimu/erp-utils/issues) or project boards.
736
+ For detailed version history, please refer to the [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/CHANGELOG.md) in the main repository.
737
+ Future plans and roadmap items are tracked via GitHub issues and milestones.
812
738
 
813
739
  ### License
814
740
 
@@ -816,7 +742,4 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
816
742
 
817
743
  ### Acknowledgments
818
744
 
819
- * Developed as part of the `@asaidimu` utilities collection.
820
- * Leverages foundational packages like [`@asaidimu/utils-store`](https://www.npmjs.com/package/@asaidimu/utils-store) and `@asaidimu/events` within the `erp-utils` monorepo.
821
- * Uses [Semantic Release](https://semantic-release.gitbook.io/semantic-release/) for automated versioning and publishing.
822
- * Utilizes [Vitest](https://vitest.dev/) for fast and reliable testing.
745
+ This library is part of the `@asaidimu/erp-utils` monorepository and integrates closely with `@core/store/types` (specifically, the `DataStore` interface) for reactive state management.