@asaidimu/utils-artifacts 7.3.0 → 8.1.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
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
+ ## ✨ Unlocking Application Power with Reactive Artifacts
6
+
7
+ The `@asaidimu/utils-artifacts` library empowers developers to build highly organized, maintainable, and performant applications by providing a sophisticated reactive dependency injection container. It excels at managing complex application states and component lifecycles, ensuring that your application's pieces stay synchronized and up-to-date with minimal manual effort. Its design focuses on **declarative dependency management** and **automatic reactivity**, allowing developers to concentrate on business logic rather than intricate synchronization and lifecycle plumbing.
8
+
5
9
  [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-artifacts.svg?style=flat-square)](https://www.npmjs.com/package/@asaidimu/utils-artifacts)
6
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
7
11
  [![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)
@@ -10,23 +14,23 @@ A powerful TypeScript library for managing application components (artifacts) wi
10
14
 
11
15
  ## 🚀 Quick Links
12
16
 
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)
17
+ - [Overview & Features](#-overview--features)
18
+ - [Why Use This Library? (Power & Use Cases)](#-why-use-this-library-power--use-cases)
19
+ - [Installation & Setup](#-installation--setup)
20
+ - [Usage Documentation](#-usage-documentation)
21
+ - [Basic Usage](#basic-usage)
22
+ - [Registering Artifacts](#registering-artifacts)
23
+ - [Resolving & Requiring Artifacts](#resolving--requiring-artifacts)
24
+ - [Watching Artifact Changes](#watching-artifact-changes)
25
+ - [Reactive Dependencies (State Selection)](#reactive-dependencies-state-selection)
26
+ - [Artifact Lifecycle (Cleanup & Dispose)](#artifact-lifecycle-cleanup--dispose)
27
+ - [Streaming Artifacts](#streaming-artifacts)
28
+ - [Invalidating Artifacts](#invalidating-artifacts)
29
+ - [Debugging Artifacts](#debugging-artifacts)
30
+ - [Project Architecture](#-project-architecture)
31
+ - [Considerations & Potential Pitfalls](#-considerations--potential-pitfalls)
32
+ - [Development & Contributing](#-development--contributing)
33
+ - [Additional Information](#-additional-information)
30
34
 
31
35
  ---
32
36
 
@@ -36,16 +40,37 @@ A powerful TypeScript library for managing application components (artifacts) wi
36
40
 
37
41
  ### Key Features
38
42
 
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.
43
+ - **Declarative Dependency Injection (DI)**: Define artifact dependencies implicitly through factory context methods (`ctx.use`, `ctx.resolve`, `ctx.require`).
44
+ - **Reactive State Management**: Automatically re-evaluate artifacts when slices of a global `DataStore` change via `ctx.select`, enabling highly reactive applications.
45
+ - **Flexible Scoping**: Supports `Singleton` (single, cached instance) and `Transient` (new instance per resolution) artifact scopes.
46
+ - **Advanced Lifecycle Management**: `onCleanup` for instance-specific resource release and `onDispose` for permanent artifact teardown.
47
+ - **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.
48
+ - **Robust Error Handling & Retries**: Integrated retry logic for resilient operations and a comprehensive `ArtifactError` hierarchy for clear diagnostics.
49
+ - **Concurrency Primitives**: Internal utilities for managing race conditions and sequential execution of async operations.
50
+ - **Debuggability**: `container.debugInfo()` provides a snapshot of the artifact graph, statuses, and dependencies for easy troubleshooting.
51
+ - **Watcher API**: `container.watch()` allows external consumers to subscribe to artifact changes without directly resolving them.
52
+ - **Circular Dependency Detection**: Prevents infinite loops during resolution by detecting and reporting cycles in the dependency graph.
53
+
54
+ ---
55
+
56
+ ## 🚀 Why Use This Library? (Power & Use Cases)
57
+
58
+ The `@asaidimu/utils-artifacts` library is designed to tackle the complexities of modern application development by providing a robust, reactive, and well-structured approach to managing application components and their interdependencies.
59
+
60
+ ### Core Power
61
+
62
+ - **Automatic Reactivity**: Effortlessly build applications where components automatically update when underlying data or services change. This dramatically simplifies state synchronization and UI updates.
63
+ - **Sophisticated Dependency Management**: Declaring dependencies is natural and integrated. The library handles complex resolution graphs, including cycles, and ensures dependencies are met before an artifact is built.
64
+ - **Lifecycle Control**: Fine-grained control over artifact lifecycles (`Singleton`, `Transient`) with explicit hooks for cleanup and disposal ensures proper resource management, preventing leaks.
65
+ - **Decoupled Architecture**: Promotes a clean separation of concerns, making code more modular, testable, and easier to maintain.
66
+
67
+ ### Key Use Cases
68
+
69
+ - **Complex Front-End Applications**: Ideal for managing intricate application state, services, and UI components in large-scale SPAs where reactivity and component lifecycles are paramount.
70
+ - **Microservice Orchestration**: Coordinating services that depend on each other, managing their initialization, and handling their inter-service communication dependencies.
71
+ - **Real-time Data Pipelines & Dashboards**: The streaming API (`ctx.stream`) is perfect for artifacts that continuously produce data, such as WebSocket clients, data feeders, or background processing agents.
72
+ - **Shared Resource Management**: Managing singleton instances of expensive resources like API clients, database connections, or global caches with predictable lifecycles.
73
+ - **Feature Toggling & Configuration**: Dynamically loading or updating configurations and feature flags that affect multiple parts of the application.
49
74
 
50
75
  ---
51
76
 
@@ -53,9 +78,9 @@ A powerful TypeScript library for managing application components (artifacts) wi
53
78
 
54
79
  ### Prerequisites
55
80
 
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`).
81
+ - Node.js (LTS recommended)
82
+ - npm or Yarn (package manager)
83
+ - A compatible reactive data store library (e.g., `@asaidimu/utils-store`) that adheres to the `DataStore` interface (providing `watch`, `get`, `set` methods).
59
84
 
60
85
  ### Installation Steps
61
86
 
@@ -76,26 +101,26 @@ yarn add @asaidimu/utils-artifacts
76
101
  You can verify the installation by attempting to import and register a basic artifact:
77
102
 
78
103
  ```typescript
79
- import { ArtifactContainer } from '@asaidimu/utils-artifacts';
80
- import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming you have this store
104
+ import { ArtifactContainer } from "@asaidimu/utils-artifacts";
105
+ import { ReactiveDataStore } from "@asaidimu/utils-store"; // Assuming you have this store
81
106
 
82
107
  // Define your application's global state and artifact registry types
83
- type AppState = { count: number; };
84
- type AppRegistry = { myService: string; };
108
+ type AppState = { count: number };
109
+ type AppRegistry = { myService: string };
85
110
 
86
111
  const store = new ReactiveDataStore<AppState>({ count: 0 });
87
112
  const container = new ArtifactContainer<AppRegistry, AppState>(store);
88
113
 
89
114
  container.register({
90
- key: 'myService',
115
+ key: "myService",
91
116
  factory: async ({ use }) => {
92
- const currentCount = await use(({ select }) => select(s => s.count));
117
+ const currentCount = await use(({ select }) => select((s) => s.count));
93
118
  return `Service is running with count: ${currentCount}`;
94
119
  },
95
120
  });
96
121
 
97
122
  async function runExample() {
98
- const service = await container.resolve('myService');
123
+ const service = await container.resolve("myService");
99
124
  console.log(service.instance); // Expected: Service is running with count: 0
100
125
  }
101
126
 
@@ -112,7 +137,7 @@ The core of the library is the `ArtifactContainer`. You initialize it with a `Da
112
137
 
113
138
  ```typescript
114
139
  import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
115
- import { ReactiveDataStore } from '@asaidimu/utils-store';
140
+ import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming this is your state store
116
141
 
117
142
  // 1. Define your application's global state and artifact registry types
118
143
  interface AppState {
@@ -131,7 +156,7 @@ interface AppRegistry {
131
156
  appInfo: string;
132
157
  }
133
158
 
134
- // Example DataStore (from @asaidimu/utils-store)
159
+ // Example DataStore (from @asaidimu/utils-store or similar)
135
160
  const appStore = new ReactiveDataStore<AppState>({
136
161
  appName: 'My App',
137
162
  version: '1.0.0',
@@ -144,7 +169,7 @@ const appStore = new ReactiveDataStore<AppState>({
144
169
  // 2. Instantiate the ArtifactContainer
145
170
  const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
146
171
 
147
- // 3. Define and register your artifact factories
172
+ // Mock services for demonstration
148
173
  class LoggerService {
149
174
  constructor(private prefix: string) {}
150
175
  log(message: string) {
@@ -152,6 +177,11 @@ class LoggerService {
152
177
  }
153
178
  }
154
179
 
180
+ interface ApiClient {
181
+ fetchData(path: string): Promise<{ message: string }>;
182
+ }
183
+
184
+ // 3. Define and register your artifact factories
155
185
  container.register({
156
186
  key: 'logger',
157
187
  scope: ArtifactScopes.Singleton, // Ensure only one instance of the logger
@@ -211,10 +241,11 @@ async function main() {
211
241
  console.log(appInfo.instance); // Logs the app information after API call
212
242
 
213
243
  // Example of reacting to state changes
214
- console.log('\n--- Changing theme ---');
244
+ console.log('
245
+ --- Changing theme ---');
215
246
  await appStore.set(s => ({ ...s, config: { ...s.config, theme: 'dark' } }));
216
247
 
217
- // Because themeService depends on state.config.theme, it will be rebuilt
248
+ // Because themeService depends on state.config.theme, it will be rebuilt.
218
249
  // Any artifact that depends on themeService would also be rebuilt.
219
250
  const newTheme = await container.resolve('themeService');
220
251
  console.log('New Theme:', newTheme.instance); // Expected: dark
@@ -228,421 +259,388 @@ main();
228
259
  Use `container.register()` to define an artifact. Each artifact requires a unique `key` and a `factory` function.
229
260
 
230
261
  ```typescript
231
- import { ArtifactScopes } from '@asaidimu/utils-artifacts';
262
+ import { ArtifactScopes, ArtifactTemplate } from "@asaidimu/utils-artifacts";
232
263
 
233
- container.register({
234
- key: 'myArtifact',
235
- factory: () => 'Hello, Artifact!',
264
+ // Define your app's state and registry types for better type safety
265
+ interface MyAppState {
266
+ /* ... */
267
+ }
268
+ interface MyAppRegistry {
269
+ myService: string;
270
+ }
271
+
272
+ // Type assertion for the container
273
+ const container = new ArtifactContainer<MyAppRegistry, MyAppState>(myStore);
274
+
275
+ // Example of registering an artifact
276
+ container.register<MyAppRegistry, MyAppState, "myService">({
277
+ key: "myService",
278
+ factory: async (ctx) => {
279
+ // Access dependencies using ctx.use()
280
+ const logger = await ctx.use(({ require }) => require("logger"));
281
+ const apiUrl = await ctx.use(({ select }) =>
282
+ select((state) => state.config.apiUrl),
283
+ );
284
+
285
+ logger.log(`Initializing myService with API URL: ${apiUrl}`);
286
+ return `Initialized: ${apiUrl}`;
287
+ },
236
288
  // Optional parameters:
237
289
  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
290
+ lazy: true, // For singletons: true (default) to build on first resolve, false to build on registration.
291
+ timeout: 5000, // Max time in ms for the factory to complete.
292
+ retries: 3, // Number of retries on factory failure (external errors only).
293
+ debounce: 100, // Delay in ms before rebuilding after dependency changes.
242
294
  });
243
295
  ```
244
296
 
245
- The `factory` function receives an `ArtifactFactoryContext` object:
297
+ The `factory` function receives an `ArtifactFactoryContext` object, providing access to dependencies, state, and lifecycle management:
246
298
 
247
299
  ```typescript
300
+ /**
301
+ * Context provided to an artifact's factory function.
302
+ * @template TRegistry The full artifact registry type.
303
+ * @template TState The global state type.
304
+ * @template TArtifact The type of the artifact being created.
305
+ */
248
306
  interface ArtifactFactoryContext<TRegistry, TState, TArtifact> {
249
- state(): TState; // Get current global state (non-reactive)
250
- previous?: TArtifact; // Previous instance (for singletons on rebuild)
307
+ /** Returns the current global state object. Non-reactive. */
308
+ state(): TState;
309
+ /** The previous instance of a singleton artifact (if available). */
310
+ previous?: TArtifact;
311
+ /** Executes a callback within a dependency tracking context. */
251
312
  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)
313
+ /** Registers a cleanup function for the current artifact instance. */
314
+ onCleanup(cleanup: ArtifactCleanup): void;
315
+ /** Registers a cleanup function for when the artifact is permanently disposed. */
316
+ onDispose(callback: ArtifactCleanup): void;
317
+ /** Starts a streaming process for a Singleton artifact. */
318
+ stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => ...): void;
255
319
  }
256
320
 
321
+ /**
322
+ * Context for resolving dependencies within `use()` callback.
323
+ * @template TRegistry The full artifact registry type.
324
+ * @template TState The global state type.
325
+ */
257
326
  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)
327
+ /** Resolves another artifact (returns `ResolvedArtifact`). */
328
+ resolve<K extends keyof TRegistry>(key: K): Promise<ResolvedArtifact<TRegistry[K]>>;
329
+ /** Resolves an artifact and throws on error (returns instance directly). */
330
+ require<K extends keyof TRegistry>(key: K): Promise<TRegistry[K]>;
331
+ /** Selects a slice of global state reactively. */
332
+ select<S>(selector: (state: TState) => S): S;
261
333
  }
262
334
  ```
263
335
 
264
336
  ### Resolving & Requiring Artifacts
265
337
 
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.
338
+ - `container.resolve(key)`: Returns a `Promise<ResolvedArtifact<T>>`. This union type represents the artifact's state: `ReadyArtifact`, `ErrorArtifact`, or `PendingArtifact`. Check `.ready` and `.error` properties for robust handling.
339
+ - `container.require(key)`: Returns a `Promise<T>` directly. It throws an error if resolution fails. Use this when you expect success and prefer exception-based error handling.
268
340
 
269
341
  ```typescript
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);
342
+ // Using resolve for defensive programming
343
+ const myServiceResult = await container.resolve("myService");
344
+ if (myServiceResult.ready) {
345
+ console.log("Service instance:", myServiceResult.instance);
346
+ } else if (myServiceResult.error) {
347
+ console.error("Service failed:", myServiceResult.error);
278
348
  } else {
279
- console.log('Artifact is pending/idle.');
349
+ console.log("Service is pending or idle.");
280
350
  }
281
351
 
282
- // Using require (for simpler usage when errors are handled upstream or unexpected)
352
+ // Using require for direct access when errors are handled upstream
283
353
  try {
284
- const myArtifactInstance = await container.require('myArtifact');
285
- console.log('Artifact instance:', myArtifactInstance);
354
+ const myServiceInstance = await container.require("myService");
355
+ console.log("Service instance:", myServiceInstance);
286
356
  } 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
- }
357
+ console.error("Failed to get service:", error);
292
358
  }
293
359
  ```
294
360
 
295
361
  ### Watching Artifact Changes
296
362
 
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.
363
+ The `container.watch(key)` method returns an `ArtifactObserver`. This allows you to subscribe to artifact state changes without directly resolving them, ideal for UI updates.
298
364
 
299
365
  ```typescript
300
- const myServiceWatcher = container.watch('myService');
366
+ const loggerWatcher = container.watch("logger");
301
367
 
302
- const unsubscribe = myServiceWatcher.subscribe((resolvedArtifact) => {
368
+ const unsubscribe = loggerWatcher.subscribe((resolvedArtifact) => {
303
369
  if (resolvedArtifact.ready) {
304
- console.log('myService updated:', resolvedArtifact.instance);
370
+ console.log("Logger updated:", resolvedArtifact.instance);
305
371
  } else if (resolvedArtifact.error) {
306
- console.error('myService error:', resolvedArtifact.error);
372
+ console.error("Logger error:", resolvedArtifact.error);
307
373
  }
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);
374
+ // `get()` retrieves the current resolved artifact state without subscribing.
375
+ const currentLogger = loggerWatcher.get();
376
+ console.log("Current logger:", currentLogger.instance);
312
377
  });
313
378
 
314
- // To stop receiving updates:
315
- unsubscribe();
379
+ // To stop listening for updates:
380
+ // unsubscribe();
316
381
  ```
317
382
 
318
383
  ### Reactive Dependencies (State Selection)
319
384
 
320
- Artifacts can react to changes in the global `DataStore` by using `ctx.select()`.
385
+ Artifacts can automatically react to changes in your application's global state by using `ctx.select()` within the `use` callback.
321
386
 
322
387
  ```typescript
323
- interface UserSettings { userId: string; theme: string; };
324
- type AppRegistry = { userPreference: string };
388
+ interface AppState {
389
+ user: { name: string; theme: "light" | "dark" };
390
+ }
391
+ interface AppRegistry {
392
+ userGreeting: string;
393
+ }
325
394
 
326
- const userStore = new ReactiveDataStore<UserSettings>({ userId: 'guest', theme: 'light' });
327
- const userContainer = new ArtifactContainer<AppRegistry, UserSettings>(userStore);
395
+ // Assume appStore is an instance of DataStore<AppState>
396
+ const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
328
397
 
329
- userContainer.register({
330
- key: 'userPreference',
398
+ container.register({
399
+ key: "userGreeting",
331
400
  factory: async ({ use }) => {
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}`;
401
+ // This artifact will rebuild if appStore.user.theme changes.
402
+ const theme = await use(({ select }) =>
403
+ select((state) => state.user.theme),
404
+ );
405
+ const userName = await use(({ select }) =>
406
+ select((state) => state.user.name),
407
+ );
408
+ return `Hello, ${userName}! Your theme is ${theme}.`;
335
409
  },
336
410
  });
337
411
 
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
348
- }
349
-
350
- runReactiveExample();
412
+ // Example of state change triggering rebuild
413
+ await appStore.set((state) => ({
414
+ ...state,
415
+ user: { ...state.user, theme: "dark" },
416
+ }));
417
+ const greeting = await container.resolve("userGreeting");
418
+ console.log(greeting.instance); // Output will reflect the new theme.
351
419
  ```
352
420
 
353
421
  ### Artifact Lifecycle (Cleanup & Dispose)
354
422
 
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).
423
+ - `ctx.onCleanup(fn)`: Registers a function to be executed _before_ a singleton artifact's instance is replaced or before a transient artifact instance is discarded. Useful for releasing resources tied to the _current instance_ (e.g., clearing timers, unsubscribing local event listeners).
424
+ - `ctx.onDispose(fn)`: Registers a function to be executed _only when the artifact is permanently unregistered_ from the container or when the container itself is disposed. Use this for final resource cleanup (e.g., closing database connections).
357
425
 
358
426
  ```typescript
359
427
  container.register({
360
- key: 'myResource',
428
+ key: "resourceArtifact",
361
429
  scope: ArtifactScopes.Singleton,
362
- factory: ({ onCleanup, onDispose }) => {
363
- const resource = { id: Math.random(), intervalId: setInterval(() => {}, 1000) };
364
- console.log(`Resource ${resource.id} created.`);
430
+ factory: ({ onCleanup, onDispose, use }) => {
431
+ const resourceId = Math.random();
432
+ console.log(`Resource ${resourceId} created.`);
433
+ const timerId = setInterval(
434
+ () => console.log(`Resource ${resourceId} heartbeat...`),
435
+ 1000,
436
+ );
365
437
 
438
+ // Cleanup for the current instance (e.g., on rebuild or if transient)
366
439
  onCleanup(() => {
367
- console.log(`Cleaning up instance ${resource.id}...`);
368
- clearInterval(resource.intervalId);
440
+ console.log(`Cleaning up instance for resource ${resourceId}...`);
441
+ clearInterval(timerId);
369
442
  });
370
443
 
444
+ // Dispose for permanent removal from container
371
445
  onDispose(() => {
372
- console.log(`Disposing artifact 'myResource'.`);
373
- // Additional permanent resource release here
446
+ console.log(
447
+ `Disposing artifact 'resourceArtifact' (resource ${resourceId}).`,
448
+ );
449
+ // Final resource release
374
450
  });
375
451
 
376
- return resource;
452
+ return { id: resourceId };
377
453
  },
378
454
  });
379
455
 
380
456
  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.
457
+ await container.resolve("resourceArtifact"); // Instance created and logs "heartbeat..."
458
+ console.log("Artifact resolved.");
385
459
 
386
- // When unregistering, onDispose is called
387
- await container.unregister('myResource'); // Triggers onCleanup (if active), then onDispose
460
+ // Simulate invalidation: will trigger onCleanup for the current instance, then rebuild.
461
+ await container.invalidate("resourceArtifact");
462
+ console.log("Artifact invalidated and rebuilt.");
463
+
464
+ // Manually dispose the artifact and container
465
+ await container.unregister("resourceArtifact"); // Triggers onDispose
466
+ console.log("Artifact unregistered.");
388
467
  }
389
468
  lifecycleExample();
390
469
  ```
391
470
 
392
471
  ### Streaming Artifacts
393
472
 
394
- Singletons can continuously emit new values using `ctx.stream()`. This is powerful for reactive data sources.
473
+ For artifacts that continuously emit values (e.g., real-time data, streams), use `ctx.stream()`. This is only supported for `Singleton` artifacts.
395
474
 
396
475
  ```typescript
397
476
  container.register({
398
- key: 'counterStream',
477
+ key: "dataStream",
399
478
  scope: ArtifactScopes.Singleton,
400
- factory: ({ stream, onCleanup }) => {
401
- let count = 0;
402
- let interval: ReturnType<typeof setInterval>;
479
+ factory: ({ stream, onCleanup, use }) => {
480
+ let counter = 0;
481
+ let intervalId: ReturnType<typeof setInterval> | undefined;
482
+
483
+ stream(async ({ emit, signal, value }) => {
484
+ console.log("Data stream started...");
485
+ const logger = await use(({ require }) => require("logger")); // Example dependency
486
+ logger.log("Stream active. Emitting values.");
403
487
 
404
- stream(async ({ emit, signal }) => {
405
- console.log('Counter stream started...');
406
- interval = setInterval(() => {
488
+ intervalId = setInterval(() => {
407
489
  if (signal.aborted) {
408
- console.log('Stream aborted, stopping interval.');
409
- clearInterval(interval);
490
+ console.log("Stream signal aborted. Clearing interval.");
491
+ clearInterval(intervalId);
410
492
  return;
411
493
  }
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
494
+ counter++;
495
+ const newValue = `Data update #${counter}`;
496
+ console.log(`Emitting: ${newValue}`);
497
+ emit(newValue); // Emit the new value to the container
498
+ if (counter >= 5) {
499
+ console.log("Stream reached limit, stopping.");
500
+ clearInterval(intervalId);
501
+ // The factory function can return to stop the stream producer.
418
502
  }
419
503
  }, 500);
420
504
  });
421
505
 
422
506
  onCleanup(() => {
423
- console.log('Cleaning up counter stream instance...');
424
- // Ensure interval is cleared if stream is aborted/rebuilt
425
- clearInterval(interval);
507
+ console.log("Cleaning up data stream instance...");
508
+ // Ensure interval is cleared if stream is aborted or rebuilt.
509
+ if (intervalId) clearInterval(intervalId);
426
510
  });
427
511
 
428
- return 0; // Initial value before stream starts emitting
512
+ return "Initial stream value"; // Initial value before first emission
429
513
  },
430
514
  });
431
515
 
432
516
  async function streamExample() {
433
- const watcher = container.watch('counterStream');
517
+ const watcher = container.watch("dataStream");
434
518
  const unsubscribe = watcher.subscribe((art) => {
435
- if (art.ready) console.log('Counter value:', art.instance);
519
+ if (art.ready) console.log("Stream received:", art.instance);
436
520
  });
437
521
 
438
- // Keep alive for a few seconds to see stream emissions
439
- await new Promise(res => setTimeout(res, 3000));
522
+ // Keep alive for a few seconds to observe stream emissions
523
+ await new Promise((res) => setTimeout(res, 3000));
440
524
  unsubscribe();
441
- console.log('Stream watcher unsubscribed.');
525
+ console.log("Stream watcher unsubscribed.");
442
526
  }
443
527
  streamExample();
444
528
  ```
445
529
 
446
530
  ### Invalidating Artifacts
447
531
 
448
- You can manually trigger an artifact to rebuild, which will also cascade invalidations to its dependents.
532
+ Manually trigger an artifact to rebuild and cascade invalidations to its dependents.
449
533
 
450
534
  ```typescript
451
- // Invalidate a specific artifact
452
- await container.invalidate('myArtifact');
535
+ // Invalidate an artifact; it will rebuild if it has dependents, is lazy and watched, or eagerly.
536
+ await container.invalidate("myArtifact");
453
537
 
454
- // Force immediate rebuild, bypassing any debounce delay
455
- await container.invalidate('myArtifact', true);
538
+ // Force immediate rebuild, bypassing any debounce delay.
539
+ await container.invalidate("myArtifact", true);
456
540
  ```
457
541
 
458
542
  ### Debugging Artifacts
459
543
 
460
- The `debugInfo()` method provides a snapshot of the container's internal state, useful for understanding dependencies, status, and build counts.
544
+ The `container.debugInfo()` method provides a snapshot of the container's internal state, which is invaluable for understanding dependencies, status, and build counts.
461
545
 
462
546
  ```typescript
463
547
  const debugNodes = container.debugInfo();
464
- debugNodes.forEach(node => {
465
- console.log(`\nID: ${node.id}`);
548
+ debugNodes.forEach((node) => {
549
+ console.log(`
550
+ Artifact ID: ${node.id}`);
466
551
  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'}`);
552
+ console.log(` Status: ${node.status}`); // e.g., 'active', 'error', 'idle', 'building', 'pending', 'debouncing'
553
+ console.log(
554
+ ` Dependencies (Artifacts): ${node.dependencies.join(", ") || "None"}`,
555
+ );
556
+ console.log(
557
+ ` Dependencies (State Paths): ${node.stateDependencies.join(", ") || "None"}`,
558
+ );
559
+ console.log(` Dependents: ${node.dependents.join(", ") || "None"}`);
471
560
  console.log(` Build Count: ${node.buildCount}`);
472
561
  });
473
562
  ```
474
563
 
475
- ### Retry Utility
564
+ ---
476
565
 
477
- The `Retry` class provides flexible retry logic for any async operation, supporting various strategies.
566
+ ## 🏗️ Project Architecture
478
567
 
479
- ```typescript
480
- import { Retry, RetryExhaustedError, RetryPredicates } from '@asaidimu/utils-artifacts/retry';
568
+ The `@asaidimu/utils-artifacts` library is architected around a central `ArtifactContainer` that orchestrates several specialized internal components:
481
569
 
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
- };
490
-
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
- }
570
+ - **`ArtifactContainer`**: The main public API. It aggregates and coordinates the other components, providing methods for registering, resolving, watching, invalidating, and disposing artifacts.
571
+ - **`ArtifactRegistry`**: Stores the definitions (`ArtifactTemplate`s) for all artifacts. It maps artifact keys to their factories and configuration options.
572
+ - **`ArtifactCache`**: Manages the lifecycle and instances of resolved artifacts. It stores singleton instances, tracks artifact versions, and caches results.
573
+ - **`ArtifactDependencyGraph`**: Maintains the relationships between artifacts and their dependencies on global state paths. This graph is critical for detecting circular dependencies and for efficiently cascading invalidations.
574
+ - **`ArtifactManager`**: The core engine responsible for artifact lifecycle management. It handles artifact building (executing factories), managing retries, timeouts, stream propagation, and the reactive invalidation process. It utilizes the other components for dependency resolution and state observation.
575
+ - **`ArtifactObserverManager`**: Implements the `container.watch()` API. It manages subscriptions from external consumers, notifies them of artifact state changes, and tracks active watchers to manage lazy loading and resource cleanup.
515
576
 
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
- };
577
+ ### Data Flow Example (Resolution & Reactivity)
538
578
 
539
- // const data = await fetchWithRetry('https://api.example.com/data');
540
- }
541
- let retryAttempt = 0; // Used for demonstration purposes only
542
- runRetryExample();
543
- ```
579
+ 1. **Registration**: An artifact (`key`, `factory`, `scope`, etc.) is registered with the `ArtifactRegistry`. The `ArtifactDependencyGraph` registers a node for this artifact.
580
+ 2. **Resolution (`container.resolve`)**:
581
+ - The `ArtifactContainer` delegates to `ArtifactManager`.
582
+ - The `Manager` checks the `ArtifactCache`. If a `Singleton` is already built and valid, its instance is returned.
583
+ - If not cached or if an invalidation occurred, the `Manager` retrieves the artifact's `ArtifactTemplate` from the `Registry`.
584
+ - The `Manager` prepares the `ArtifactFactoryContext` and executes the artifact's `factory` function.
585
+ - **Dependency Declaration**:
586
+ - Inside the factory, `ctx.use(({ resolve/require }) => ...)` calls recursively trigger artifact resolution, updating the `ArtifactDependencyGraph`.
587
+ - `ctx.use(({ select }) => ...)` registers state path dependencies. The `Manager` sets up listeners on the `DataStore` for these paths.
588
+ - **Build Execution**: The `Manager` handles potential retries, timeouts, and cleanup logic.
589
+ - **Cache Update**: Upon successful build, the instance is stored in the `ArtifactCache`. The `ArtifactDependencyGraph` is updated with the artifact's declared dependencies.
590
+ 3. **Reactivity & Invalidation**:
591
+ - If a `DataStore` path watched by an artifact changes, the `Manager` is notified.
592
+ - The `Manager` identifies artifacts dependent on that path using the `ArtifactDependencyGraph`.
593
+ - These dependent artifacts are marked as invalid. If they are `Singleton`s, their `onCleanup` hooks are called, and they are scheduled for rebuild (potentially with debouncing).
594
+ - If a dependency artifact is rebuilt or invalidated, its dependents are similarly cascaded.
595
+ - When an artifact rebuilds, its `factory` is executed again, re-evaluating its dependencies and state selections.
596
+ - `ArtifactObserverManager` is notified of changes to update any watching consumers.
597
+
598
+ ### Core Concepts
599
+
600
+ - **Artifact**: A unit of work or data within the application, defined by a `key` and a `factory` function.
601
+ - **Scope**: Determines the lifecycle: `Singleton` (one instance) or `Transient` (new instance per resolution).
602
+ - **Factory**: A function that creates an artifact's instance, declaring its dependencies and side effects.
603
+ - **Dependencies**: Artifacts can depend on other artifacts (`resolve`/`require`) or state slices (`select`).
604
+ - **Reactivity**: Artifacts automatically update when their declared dependencies change.
544
605
 
545
- ### Concurrency Utilities (Once & Serializer)
606
+ ---
546
607
 
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.
608
+ ## 📜 Considerations & Potential Pitfalls
548
609
 
549
- ```typescript
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
- };
610
+ While `@asaidimu/utils-artifacts` offers powerful features, understanding its operational nuances and potential trade-offs is key to leveraging it effectively and avoiding common issues.
559
611
 
560
- const [res1, res2, res3] = await Promise.all([
561
- initialization.do(expensiveInit),
562
- initialization.do(expensiveInit),
563
- initialization.do(expensiveInit),
564
- ]);
612
+ ### Debouncing Latency
565
613
 
566
- console.log(res1.value, res2.value, res3.value); // All will be 'Initialized Resource'
567
- // expensiveInit will be called only once.
568
- }
569
- runOnceExample();
614
+ - **Issue**: The `debounce` option allows delaying artifact rebuilds after dependency changes to aggregate rapid updates. However, overly large debounce values or frequent invalidation cascades can introduce noticeable latency in reflecting updates, especially in UI-critical paths.
615
+ - **Recommendation**: Carefully tune `debounce` values. Consider the trade-off between reducing update churn and responsiveness. For high-frequency updates, explore streaming or alternative reactivity patterns if latency becomes an issue.
570
616
 
571
- async function runSerializerExample() {
572
- const queue = new Serializer<string>();
573
- const order: string[] = [];
617
+ ### Wasted Build Effort from Late Staleness Detection
574
618
 
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
- };
619
+ - **Issue**: The library detects if dependencies have changed _after_ an artifact's factory has already executed. This means significant computation might be discarded if a dependency was updated mid-build, leading to wasted effort.
620
+ - **Recommendation**: Optimize artifact factories to be as performant as possible. For critical, long-running builds, consider architecting them to check for dependency validity more proactively if feasible, or accept this as a trade-off for a simpler dependency tracking mechanism.
589
621
 
590
- await Promise.all([
591
- queue.do(task1),
592
- queue.do(task2),
593
- queue.do(task3),
594
- ]);
622
+ ### Efficiency of Global State Watching
595
623
 
596
- console.log(order); // Expected: ['Task 1', 'Task 2', 'Task 3']
597
- }
598
- runSerializerExample();
599
- ```
624
+ - **Observation**: The library's reactivity relies heavily on the underlying `DataStore`'s `watch` mechanism. Inefficient state selectors or overly broad subscriptions can lead to excessive artifact rebuilds and impact application performance.
625
+ - **Recommendation**:
626
+ - Write granular and performant state selectors using `ctx.select()`.
627
+ - Minimize the number of state paths an artifact subscribes to.
628
+ - Ensure the `DataStore` implementation itself is optimized for change detection and notification.
600
629
 
601
- ---
630
+ ### Startup Cost of Eager Loading
602
631
 
603
- ## 🏗️ Project Architecture
632
+ - **Issue**: Singletons registered with `lazy: false` are eagerly instantiated upon container setup. If many such artifacts are registered, this can lead to a substantial upfront cost during application startup, impacting initial load times.
633
+ - **Recommendation**: Favor `lazy: true` (the default for singletons) whenever possible. Only use eager loading for artifacts that are truly required immediately at startup or where their initialization cost is negligible.
604
634
 
605
- The `ArtifactContainer` is designed with a modular architecture, delegating responsibilities to specialized internal components:
635
+ ### Sequential Cleanup/Dispose Execution
606
636
 
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.
637
+ - **Observation**: Cleanup and dispose functions are executed sequentially. If an artifact has many dependencies with complex or time-consuming teardown routines, this sequential execution can slow down the overall teardown process for an artifact or the entire container.
638
+ - **Recommendation**: For artifacts with numerous or computationally intensive cleanup tasks, evaluate if these tasks can be safely executed concurrently (e.g., using `Promise.all` within the cleanup/dispose logic) to improve teardown performance.
615
639
 
616
- ### Data Flow
640
+ ### Dependency Graph Complexity
617
641
 
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.
637
-
638
- ### Extension Points
639
-
640
- The primary extension point is the `ArtifactFactory` function itself, which receives the `ArtifactFactoryContext`. This context allows artifacts to:
641
-
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.
642
+ - **Observation**: While the dependency graph implementation is optimized, operations on extremely large graphs (hundreds or thousands of artifacts with complex interdependencies) can incur significant computational costs during resolution, invalidation, or cycle detection.
643
+ - **Recommendation**: For applications with exceptionally large artifact graphs, consider strategies for modularizing the container or pruning less critical dependencies if performance becomes a bottleneck.
646
644
 
647
645
  ---
648
646
 
@@ -650,8 +648,6 @@ The primary extension point is the `ArtifactFactory` function itself, which rece
650
648
 
651
649
  ### Development Setup
652
650
 
653
- To set up the project for local development:
654
-
655
651
  1. **Clone the repository:**
656
652
  ```bash
657
653
  git clone https://github.com/asaidimu/erp-utils.git
@@ -663,44 +659,23 @@ To set up the project for local development:
663
659
  # or
664
660
  yarn install
665
661
  ```
666
- 3. **Build the project (if applicable, though typically handled by IDE/watch mode):**
667
- ```bash
668
- npm run build # Or `tsc` if not defined in package.json scripts
669
- ```
670
662
 
671
663
  ### Scripts
672
664
 
673
- The `package.json` defines the following scripts:
674
-
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.
665
+ - `npm test`: Runs all tests once.
666
+ - `npm test:watch`: Runs tests in watch mode, rerunning on file changes.
678
667
 
679
668
  ### Testing
680
669
 
681
- The project uses `vitest` for testing.
682
-
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.
670
+ The project uses `vitest` for testing. All new code should be accompanied by tests.
687
671
 
688
672
  ### Contributing Guidelines
689
673
 
690
- We welcome contributions! Please follow these guidelines:
691
-
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`.
700
-
701
- ### Issue Reporting
702
-
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.
674
+ 1. **Fork and Branch**: Fork the repository and create a new branch from `main`.
675
+ 2. **Code Quality**: Write clean, readable TypeScript code adhering to project conventions.
676
+ 3. **Tests**: Add unit/integration tests for all new features and bug fixes.
677
+ 4. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat: add streaming support`, `fix: correct debounce logic`).
678
+ 5. **Pull Requests**: Submit PRs with clear descriptions and link to relevant issues.
704
679
 
705
680
  ---
706
681
 
@@ -708,33 +683,28 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
708
683
 
709
684
  ### Troubleshooting
710
685
 
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.
686
+ - **`ArtifactNotFoundError`**: You are trying to `resolve` or `watch` an artifact that hasn't been registered. Verify artifact keys and ensure registration order.
687
+ - **`CircularDependencyError`**: The dependency graph contains a loop. Check the error message's path and redesign dependencies to break the cycle. `container.debugInfo()` is helpful here.
688
+ - **`TimeoutError`**: An artifact factory took too long to execute. Increase the `timeout` option or optimize the factory function and its dependencies.
689
+ - **Unexpected Rebuilds/No Rebuilds**:
690
+ - Inspect artifact status and dependencies using `container.debugInfo()`.
691
+ - Ensure `ctx.select()` selectors correctly capture reactive state slices.
692
+ - Review `debounce` settings if rebuilds are too frequent or delayed.
693
+ - Verify `onCleanup`/`onDispose` hooks are correctly implemented.
719
694
 
720
695
  ### FAQ
721
696
 
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.
697
+ - **`Singleton` vs. `Transient` Scope**:
698
+ - **`Singleton`**: Only one instance is ever created and shared across all resolutions. Ideal for services, configurations, or shared resources. Supports lifecycle hooks (`onCleanup`, `onDispose`) and streaming.
699
+ - **`Transient`**: A new instance is created every time the artifact is resolved. Useful for ephemeral objects. Does not support streaming or persistent `onDispose`.
700
+ - **`resolve` vs. `require`**:
701
+ - Use `resolve` for robust handling of artifact states (ready, error, pending) via the `ResolvedArtifact` object.
702
+ - Use `require` when you expect immediate success and want errors to propagate as exceptions.
703
+ - **`onCleanup` vs. `onDispose`**:
704
+ - `onCleanup`: Runs before an instance is replaced (Singleton rebuild) or discarded (Transient). Use for instance-specific resource cleanup.
705
+ - `onDispose`: Runs only when the artifact is permanently removed from the container. Use for global resource cleanup.
733
706
 
734
- ### Changelog/Roadmap
735
-
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.
707
+ ---
738
708
 
739
709
  ### License
740
710
 
@@ -742,4 +712,4 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
742
712
 
743
713
  ### Acknowledgments
744
714
 
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.
715
+ This library is part of the `@asaidimu/erp-utils` monorepository and relies on a compatible `DataStore` implementation for reactive state management.