@asaidimu/utils-artifacts 7.3.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -333
- package/index.d.mts +25 -24
- package/index.d.ts +25 -24
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/@asaidimu/utils-artifacts)
|
|
6
10
|
[](https://opensource.org/licenses/MIT)
|
|
7
11
|
[](https://github.com/asaidimu/erp-utils/actions/workflows/ci.yml)
|
|
@@ -11,6 +15,7 @@ A powerful TypeScript library for managing application components (artifacts) wi
|
|
|
11
15
|
## 🚀 Quick Links
|
|
12
16
|
|
|
13
17
|
- [Overview & Features](#-overview--features)
|
|
18
|
+
- [Why Use This Library? (Power & Use Cases)](#-why-use-this-library-power--use-cases)
|
|
14
19
|
- [Installation & Setup](#-installation--setup)
|
|
15
20
|
- [Usage Documentation](#-usage-documentation)
|
|
16
21
|
- [Basic Usage](#basic-usage)
|
|
@@ -22,9 +27,8 @@ A powerful TypeScript library for managing application components (artifacts) wi
|
|
|
22
27
|
- [Streaming Artifacts](#streaming-artifacts)
|
|
23
28
|
- [Invalidating Artifacts](#invalidating-artifacts)
|
|
24
29
|
- [Debugging Artifacts](#debugging-artifacts)
|
|
25
|
-
- [Retry Utility](#retry-utility)
|
|
26
|
-
- [Concurrency Utilities (Once & Serializer)](#concurrency-utilities-once--serializer)
|
|
27
30
|
- [Project Architecture](#-project-architecture)
|
|
31
|
+
- [Considerations & Potential Pitfalls](#-considerations--potential-pitfalls)
|
|
28
32
|
- [Development & Contributing](#-development--contributing)
|
|
29
33
|
- [Additional Information](#-additional-information)
|
|
30
34
|
|
|
@@ -36,26 +40,47 @@ A powerful TypeScript library for managing application components (artifacts) wi
|
|
|
36
40
|
|
|
37
41
|
### Key Features
|
|
38
42
|
|
|
39
|
-
* **Dependency Injection (DI)**:
|
|
40
|
-
* **Reactive State Management**: Automatically re-evaluate artifacts when slices of a global `DataStore`
|
|
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.
|
|
41
45
|
* **Flexible Scoping**: Supports `Singleton` (single, cached instance) and `Transient` (new instance per resolution) artifact scopes.
|
|
42
|
-
* **Advanced Lifecycle Management**: `onCleanup` for instance-specific
|
|
46
|
+
* **Advanced Lifecycle Management**: `onCleanup` for instance-specific resource release and `onDispose` for permanent artifact teardown.
|
|
43
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.
|
|
44
|
-
* **Robust Error Handling & Retries**: Integrated retry logic
|
|
45
|
-
* **Concurrency Primitives**: Internal
|
|
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.
|
|
46
50
|
* **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
|
|
51
|
+
* **Watcher API**: `container.watch()` allows external consumers to subscribe to artifact changes without directly resolving them.
|
|
48
52
|
* **Circular Dependency Detection**: Prevents infinite loops during resolution by detecting and reporting cycles in the dependency graph.
|
|
49
53
|
|
|
50
54
|
---
|
|
51
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.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
52
77
|
## 📦 Installation & Setup
|
|
53
78
|
|
|
54
79
|
### Prerequisites
|
|
55
80
|
|
|
56
81
|
* Node.js (LTS recommended)
|
|
57
82
|
* npm or Yarn (package manager)
|
|
58
|
-
* A reactive data store library that adheres to the `DataStore` interface (
|
|
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
|
|
|
@@ -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
|
-
//
|
|
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('
|
|
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,214 +259,247 @@ 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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
264
|
+
// Define your app's state and registry types for better type safety
|
|
265
|
+
interface MyAppState { /* ... */ }
|
|
266
|
+
interface MyAppRegistry { myService: string; }
|
|
267
|
+
|
|
268
|
+
// Type assertion for the container
|
|
269
|
+
const container = new ArtifactContainer<MyAppRegistry, MyAppState>(myStore);
|
|
270
|
+
|
|
271
|
+
// Example of registering an artifact
|
|
272
|
+
container.register<MyAppRegistry, MyAppState, 'myService'>({
|
|
273
|
+
key: 'myService',
|
|
274
|
+
factory: async (ctx) => {
|
|
275
|
+
// Access dependencies using ctx.use()
|
|
276
|
+
const logger = await ctx.use(({ require }) => require('logger'));
|
|
277
|
+
const apiUrl = await ctx.use(({ select }) => select(state => state.config.apiUrl));
|
|
278
|
+
|
|
279
|
+
logger.log(`Initializing myService with API URL: ${apiUrl}`);
|
|
280
|
+
return `Initialized: ${apiUrl}`;
|
|
281
|
+
},
|
|
236
282
|
// Optional parameters:
|
|
237
283
|
scope: ArtifactScopes.Singleton, // 'singleton' (default) or 'transient'
|
|
238
|
-
lazy: true, // true (default)
|
|
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
|
|
284
|
+
lazy: true, // For singletons: true (default) to build on first resolve, false to build on registration.
|
|
285
|
+
timeout: 5000, // Max time in ms for the factory to complete.
|
|
286
|
+
retries: 3, // Number of retries on factory failure (external errors only).
|
|
287
|
+
debounce: 100, // Delay in ms before rebuilding after dependency changes.
|
|
242
288
|
});
|
|
243
289
|
```
|
|
244
290
|
|
|
245
|
-
The `factory` function receives an `ArtifactFactoryContext` object:
|
|
291
|
+
The `factory` function receives an `ArtifactFactoryContext` object, providing access to dependencies, state, and lifecycle management:
|
|
246
292
|
|
|
247
293
|
```typescript
|
|
294
|
+
/**
|
|
295
|
+
* Context provided to an artifact's factory function.
|
|
296
|
+
* @template TRegistry The full artifact registry type.
|
|
297
|
+
* @template TState The global state type.
|
|
298
|
+
* @template TArtifact The type of the artifact being created.
|
|
299
|
+
*/
|
|
248
300
|
interface ArtifactFactoryContext<TRegistry, TState, TArtifact> {
|
|
249
|
-
|
|
250
|
-
|
|
301
|
+
/** Returns the current global state object. Non-reactive. */
|
|
302
|
+
state(): TState;
|
|
303
|
+
/** The previous instance of a singleton artifact (if available). */
|
|
304
|
+
previous?: TArtifact;
|
|
305
|
+
/** Executes a callback within a dependency tracking context. */
|
|
251
306
|
use<R>(callback: (ctx: UseDependencyContext<TRegistry, TState>) => R | Promise<R>): Promise<R>;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
307
|
+
/** Registers a cleanup function for the current artifact instance. */
|
|
308
|
+
onCleanup(cleanup: ArtifactCleanup): void;
|
|
309
|
+
/** Registers a cleanup function for when the artifact is permanently disposed. */
|
|
310
|
+
onDispose(callback: ArtifactCleanup): void;
|
|
311
|
+
/** Starts a streaming process for a Singleton artifact. */
|
|
312
|
+
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => ...): void;
|
|
255
313
|
}
|
|
256
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Context for resolving dependencies within `use()` callback.
|
|
317
|
+
* @template TRegistry The full artifact registry type.
|
|
318
|
+
* @template TState The global state type.
|
|
319
|
+
*/
|
|
257
320
|
interface UseDependencyContext<TRegistry, TState> {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
321
|
+
/** Resolves another artifact (returns `ResolvedArtifact`). */
|
|
322
|
+
resolve<K extends keyof TRegistry>(key: K): Promise<ResolvedArtifact<TRegistry[K]>>;
|
|
323
|
+
/** Resolves an artifact and throws on error (returns instance directly). */
|
|
324
|
+
require<K extends keyof TRegistry>(key: K): Promise<TRegistry[K]>;
|
|
325
|
+
/** Selects a slice of global state reactively. */
|
|
326
|
+
select<S>(selector: (state: TState) => S): S;
|
|
261
327
|
}
|
|
262
328
|
```
|
|
263
329
|
|
|
264
330
|
### Resolving & Requiring Artifacts
|
|
265
331
|
|
|
266
|
-
* `container.resolve(key)`: Returns a `Promise<ResolvedArtifact<T>>`.
|
|
267
|
-
* `container.require(key)`: Returns a `Promise<T>` directly.
|
|
332
|
+
* `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.
|
|
333
|
+
* `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
334
|
|
|
269
335
|
```typescript
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
console.
|
|
276
|
-
} else if (myArtifactResult.error) {
|
|
277
|
-
console.error('Artifact failed to resolve:', myArtifactResult.error);
|
|
336
|
+
// Using resolve for defensive programming
|
|
337
|
+
const myServiceResult = await container.resolve('myService');
|
|
338
|
+
if (myServiceResult.ready) {
|
|
339
|
+
console.log('Service instance:', myServiceResult.instance);
|
|
340
|
+
} else if (myServiceResult.error) {
|
|
341
|
+
console.error('Service failed:', myServiceResult.error);
|
|
278
342
|
} else {
|
|
279
|
-
console.log('
|
|
343
|
+
console.log('Service is pending or idle.');
|
|
280
344
|
}
|
|
281
345
|
|
|
282
|
-
// Using require
|
|
346
|
+
// Using require for direct access when errors are handled upstream
|
|
283
347
|
try {
|
|
284
|
-
const
|
|
285
|
-
console.log('
|
|
348
|
+
const myServiceInstance = await container.require('myService');
|
|
349
|
+
console.log('Service instance:', myServiceInstance);
|
|
286
350
|
} catch (error) {
|
|
287
|
-
|
|
288
|
-
console.error('Artifact system error:', error.message);
|
|
289
|
-
} else {
|
|
290
|
-
console.error('Artifact runtime error:', error);
|
|
291
|
-
}
|
|
351
|
+
console.error('Failed to get service:', error);
|
|
292
352
|
}
|
|
293
353
|
```
|
|
294
354
|
|
|
295
355
|
### Watching Artifact Changes
|
|
296
356
|
|
|
297
|
-
The `watch()` method
|
|
357
|
+
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
358
|
|
|
299
359
|
```typescript
|
|
300
|
-
const
|
|
360
|
+
const loggerWatcher = container.watch('logger');
|
|
301
361
|
|
|
302
|
-
const unsubscribe =
|
|
362
|
+
const unsubscribe = loggerWatcher.subscribe((resolvedArtifact) => {
|
|
303
363
|
if (resolvedArtifact.ready) {
|
|
304
|
-
console.log('
|
|
364
|
+
console.log('Logger updated:', resolvedArtifact.instance);
|
|
305
365
|
} else if (resolvedArtifact.error) {
|
|
306
|
-
console.error('
|
|
366
|
+
console.error('Logger error:', resolvedArtifact.error);
|
|
307
367
|
}
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.log('Current state from get():', current.instance);
|
|
368
|
+
// `get()` retrieves the current resolved artifact state without subscribing.
|
|
369
|
+
const currentLogger = loggerWatcher.get();
|
|
370
|
+
console.log('Current logger:', currentLogger.instance);
|
|
312
371
|
});
|
|
313
372
|
|
|
314
|
-
// To stop
|
|
315
|
-
unsubscribe();
|
|
373
|
+
// To stop listening for updates:
|
|
374
|
+
// unsubscribe();
|
|
316
375
|
```
|
|
317
376
|
|
|
318
377
|
### Reactive Dependencies (State Selection)
|
|
319
378
|
|
|
320
|
-
Artifacts can react to changes in
|
|
379
|
+
Artifacts can automatically react to changes in your application's global state by using `ctx.select()` within the `use` callback.
|
|
321
380
|
|
|
322
381
|
```typescript
|
|
323
|
-
interface
|
|
324
|
-
|
|
382
|
+
interface AppState {
|
|
383
|
+
user: { name: string; theme: 'light' | 'dark'; };
|
|
384
|
+
}
|
|
385
|
+
interface AppRegistry { userGreeting: string; }
|
|
325
386
|
|
|
326
|
-
|
|
327
|
-
const
|
|
387
|
+
// Assume appStore is an instance of DataStore<AppState>
|
|
388
|
+
const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
|
|
328
389
|
|
|
329
|
-
|
|
330
|
-
key: '
|
|
390
|
+
container.register({
|
|
391
|
+
key: 'userGreeting',
|
|
331
392
|
factory: async ({ use }) => {
|
|
332
|
-
// This artifact will
|
|
333
|
-
const theme = await use(({ select }) => select(state => state.theme));
|
|
334
|
-
|
|
393
|
+
// This artifact will rebuild if appStore.user.theme changes.
|
|
394
|
+
const theme = await use(({ select }) => select(state => state.user.theme));
|
|
395
|
+
const userName = await use(({ select }) => select(state => state.user.name));
|
|
396
|
+
return `Hello, ${userName}! Your theme is ${theme}.`;
|
|
335
397
|
},
|
|
336
398
|
});
|
|
337
399
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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();
|
|
400
|
+
// Example of state change triggering rebuild
|
|
401
|
+
await appStore.set(state => ({ ...state, user: { ...state.user, theme: 'dark' } }));
|
|
402
|
+
const greeting = await container.resolve('userGreeting');
|
|
403
|
+
console.log(greeting.instance); // Output will reflect the new theme.
|
|
351
404
|
```
|
|
352
405
|
|
|
353
406
|
### Artifact Lifecycle (Cleanup & Dispose)
|
|
354
407
|
|
|
355
|
-
* `ctx.onCleanup(fn)`: Registers a function to
|
|
356
|
-
* `ctx.onDispose(fn)`: Registers a function to
|
|
408
|
+
* `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).
|
|
409
|
+
* `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
410
|
|
|
358
411
|
```typescript
|
|
359
412
|
container.register({
|
|
360
|
-
key: '
|
|
413
|
+
key: 'resourceArtifact',
|
|
361
414
|
scope: ArtifactScopes.Singleton,
|
|
362
|
-
factory: ({ onCleanup, onDispose }) => {
|
|
363
|
-
const
|
|
364
|
-
console.log(`Resource ${
|
|
415
|
+
factory: ({ onCleanup, onDispose, use }) => {
|
|
416
|
+
const resourceId = Math.random();
|
|
417
|
+
console.log(`Resource ${resourceId} created.`);
|
|
418
|
+
const timerId = setInterval(() => console.log(`Resource ${resourceId} heartbeat...`), 1000);
|
|
365
419
|
|
|
420
|
+
// Cleanup for the current instance (e.g., on rebuild or if transient)
|
|
366
421
|
onCleanup(() => {
|
|
367
|
-
console.log(`Cleaning up instance ${
|
|
368
|
-
clearInterval(
|
|
422
|
+
console.log(`Cleaning up instance for resource ${resourceId}...`);
|
|
423
|
+
clearInterval(timerId);
|
|
369
424
|
});
|
|
370
425
|
|
|
426
|
+
// Dispose for permanent removal from container
|
|
371
427
|
onDispose(() => {
|
|
372
|
-
console.log(`Disposing artifact '
|
|
373
|
-
//
|
|
428
|
+
console.log(`Disposing artifact 'resourceArtifact' (resource ${resourceId}).`);
|
|
429
|
+
// Final resource release
|
|
374
430
|
});
|
|
375
431
|
|
|
376
|
-
return
|
|
432
|
+
return { id: resourceId };
|
|
377
433
|
},
|
|
378
434
|
});
|
|
379
435
|
|
|
380
436
|
async function lifecycleExample() {
|
|
381
|
-
await container.resolve('
|
|
382
|
-
|
|
383
|
-
await container.invalidate('myResource'); // Triggers cleanup, then rebuilds
|
|
384
|
-
await container.resolve('myResource'); // A new instance is now resolved.
|
|
437
|
+
await container.resolve('resourceArtifact'); // Instance created and logs "heartbeat..."
|
|
438
|
+
console.log('Artifact resolved.');
|
|
385
439
|
|
|
386
|
-
//
|
|
387
|
-
await container.
|
|
440
|
+
// Simulate invalidation: will trigger onCleanup for the current instance, then rebuild.
|
|
441
|
+
await container.invalidate('resourceArtifact');
|
|
442
|
+
console.log('Artifact invalidated and rebuilt.');
|
|
443
|
+
|
|
444
|
+
// Manually dispose the artifact and container
|
|
445
|
+
await container.unregister('resourceArtifact'); // Triggers onDispose
|
|
446
|
+
console.log('Artifact unregistered.');
|
|
388
447
|
}
|
|
389
448
|
lifecycleExample();
|
|
390
449
|
```
|
|
391
450
|
|
|
392
451
|
### Streaming Artifacts
|
|
393
452
|
|
|
394
|
-
|
|
453
|
+
For artifacts that continuously emit values (e.g., real-time data, streams), use `ctx.stream()`. This is only supported for `Singleton` artifacts.
|
|
395
454
|
|
|
396
455
|
```typescript
|
|
397
456
|
container.register({
|
|
398
|
-
key: '
|
|
457
|
+
key: 'dataStream',
|
|
399
458
|
scope: ArtifactScopes.Singleton,
|
|
400
|
-
factory: ({ stream, onCleanup }) => {
|
|
401
|
-
let
|
|
402
|
-
let
|
|
459
|
+
factory: ({ stream, onCleanup, use }) => {
|
|
460
|
+
let counter = 0;
|
|
461
|
+
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
462
|
+
|
|
463
|
+
stream(async ({ emit, signal, value }) => {
|
|
464
|
+
console.log('Data stream started...');
|
|
465
|
+
const logger = await use(({ require }) => require('logger')); // Example dependency
|
|
466
|
+
logger.log('Stream active. Emitting values.');
|
|
403
467
|
|
|
404
|
-
|
|
405
|
-
console.log('Counter stream started...');
|
|
406
|
-
interval = setInterval(() => {
|
|
468
|
+
intervalId = setInterval(() => {
|
|
407
469
|
if (signal.aborted) {
|
|
408
|
-
console.log('Stream aborted
|
|
409
|
-
clearInterval(
|
|
470
|
+
console.log('Stream signal aborted. Clearing interval.');
|
|
471
|
+
clearInterval(intervalId);
|
|
410
472
|
return;
|
|
411
473
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
474
|
+
counter++;
|
|
475
|
+
const newValue = `Data update #${counter}`;
|
|
476
|
+
console.log(`Emitting: ${newValue}`);
|
|
477
|
+
emit(newValue); // Emit the new value to the container
|
|
478
|
+
if (counter >= 5) {
|
|
479
|
+
console.log('Stream reached limit, stopping.');
|
|
480
|
+
clearInterval(intervalId);
|
|
481
|
+
// The factory function can return to stop the stream producer.
|
|
418
482
|
}
|
|
419
483
|
}, 500);
|
|
420
484
|
});
|
|
421
485
|
|
|
422
486
|
onCleanup(() => {
|
|
423
|
-
console.log('Cleaning up
|
|
424
|
-
// Ensure interval is cleared if stream is aborted
|
|
425
|
-
clearInterval(
|
|
487
|
+
console.log('Cleaning up data stream instance...');
|
|
488
|
+
// Ensure interval is cleared if stream is aborted or rebuilt.
|
|
489
|
+
if (intervalId) clearInterval(intervalId);
|
|
426
490
|
});
|
|
427
491
|
|
|
428
|
-
return
|
|
492
|
+
return 'Initial stream value'; // Initial value before first emission
|
|
429
493
|
},
|
|
430
494
|
});
|
|
431
495
|
|
|
432
496
|
async function streamExample() {
|
|
433
|
-
const watcher = container.watch('
|
|
497
|
+
const watcher = container.watch('dataStream');
|
|
434
498
|
const unsubscribe = watcher.subscribe((art) => {
|
|
435
|
-
if (art.ready) console.log('
|
|
499
|
+
if (art.ready) console.log('Stream received:', art.instance);
|
|
436
500
|
});
|
|
437
501
|
|
|
438
|
-
// Keep alive for a few seconds to
|
|
502
|
+
// Keep alive for a few seconds to observe stream emissions
|
|
439
503
|
await new Promise(res => setTimeout(res, 3000));
|
|
440
504
|
unsubscribe();
|
|
441
505
|
console.log('Stream watcher unsubscribed.');
|
|
@@ -445,26 +509,27 @@ streamExample();
|
|
|
445
509
|
|
|
446
510
|
### Invalidating Artifacts
|
|
447
511
|
|
|
448
|
-
|
|
512
|
+
Manually trigger an artifact to rebuild and cascade invalidations to its dependents.
|
|
449
513
|
|
|
450
514
|
```typescript
|
|
451
|
-
// Invalidate
|
|
515
|
+
// Invalidate an artifact; it will rebuild if it has dependents, is lazy and watched, or eagerly.
|
|
452
516
|
await container.invalidate('myArtifact');
|
|
453
517
|
|
|
454
|
-
// Force immediate rebuild, bypassing any debounce delay
|
|
518
|
+
// Force immediate rebuild, bypassing any debounce delay.
|
|
455
519
|
await container.invalidate('myArtifact', true);
|
|
456
520
|
```
|
|
457
521
|
|
|
458
522
|
### Debugging Artifacts
|
|
459
523
|
|
|
460
|
-
The `debugInfo()` method provides a snapshot of the container's internal state,
|
|
524
|
+
The `container.debugInfo()` method provides a snapshot of the container's internal state, which is invaluable for understanding dependencies, status, and build counts.
|
|
461
525
|
|
|
462
526
|
```typescript
|
|
463
527
|
const debugNodes = container.debugInfo();
|
|
464
528
|
debugNodes.forEach(node => {
|
|
465
|
-
console.log(
|
|
529
|
+
console.log(`
|
|
530
|
+
Artifact ID: ${node.id}`);
|
|
466
531
|
console.log(` Scope: ${node.scope}`);
|
|
467
|
-
console.log(` Status: ${node.status}`); // active, error, idle, building, pending, debouncing
|
|
532
|
+
console.log(` Status: ${node.status}`); // e.g., 'active', 'error', 'idle', 'building', 'pending', 'debouncing'
|
|
468
533
|
console.log(` Dependencies (Artifacts): ${node.dependencies.join(', ') || 'None'}`);
|
|
469
534
|
console.log(` Dependencies (State Paths): ${node.stateDependencies.join(', ') || 'None'}`);
|
|
470
535
|
console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
|
|
@@ -472,177 +537,86 @@ debugNodes.forEach(node => {
|
|
|
472
537
|
});
|
|
473
538
|
```
|
|
474
539
|
|
|
475
|
-
|
|
540
|
+
---
|
|
476
541
|
|
|
477
|
-
|
|
542
|
+
## 🏗️ Project Architecture
|
|
478
543
|
|
|
479
|
-
|
|
480
|
-
import { Retry, RetryExhaustedError, RetryPredicates } from '@asaidimu/utils-artifacts/retry';
|
|
544
|
+
The `@asaidimu/utils-artifacts` library is architected around a central `ArtifactContainer` that orchestrates several specialized internal components:
|
|
481
545
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
}
|
|
546
|
+
* **`ArtifactContainer`**: The main public API. It aggregates and coordinates the other components, providing methods for registering, resolving, watching, invalidating, and disposing artifacts.
|
|
547
|
+
* **`ArtifactRegistry`**: Stores the definitions (`ArtifactTemplate`s) for all artifacts. It maps artifact keys to their factories and configuration options.
|
|
548
|
+
* **`ArtifactCache`**: Manages the lifecycle and instances of resolved artifacts. It stores singleton instances, tracks artifact versions, and caches results.
|
|
549
|
+
* **`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.
|
|
550
|
+
* **`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.
|
|
551
|
+
* **`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
552
|
|
|
516
|
-
|
|
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
|
-
};
|
|
553
|
+
### Data Flow Example (Resolution & Reactivity)
|
|
538
554
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
555
|
+
1. **Registration**: An artifact (`key`, `factory`, `scope`, etc.) is registered with the `ArtifactRegistry`. The `ArtifactDependencyGraph` registers a node for this artifact.
|
|
556
|
+
2. **Resolution (`container.resolve`)**:
|
|
557
|
+
* The `ArtifactContainer` delegates to `ArtifactManager`.
|
|
558
|
+
* The `Manager` checks the `ArtifactCache`. If a `Singleton` is already built and valid, its instance is returned.
|
|
559
|
+
* If not cached or if an invalidation occurred, the `Manager` retrieves the artifact's `ArtifactTemplate` from the `Registry`.
|
|
560
|
+
* The `Manager` prepares the `ArtifactFactoryContext` and executes the artifact's `factory` function.
|
|
561
|
+
* **Dependency Declaration**:
|
|
562
|
+
* Inside the factory, `ctx.use(({ resolve/require }) => ...)` calls recursively trigger artifact resolution, updating the `ArtifactDependencyGraph`.
|
|
563
|
+
* `ctx.use(({ select }) => ...)` registers state path dependencies. The `Manager` sets up listeners on the `DataStore` for these paths.
|
|
564
|
+
* **Build Execution**: The `Manager` handles potential retries, timeouts, and cleanup logic.
|
|
565
|
+
* **Cache Update**: Upon successful build, the instance is stored in the `ArtifactCache`. The `ArtifactDependencyGraph` is updated with the artifact's declared dependencies.
|
|
566
|
+
3. **Reactivity & Invalidation**:
|
|
567
|
+
* If a `DataStore` path watched by an artifact changes, the `Manager` is notified.
|
|
568
|
+
* The `Manager` identifies artifacts dependent on that path using the `ArtifactDependencyGraph`.
|
|
569
|
+
* 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).
|
|
570
|
+
* If a dependency artifact is rebuilt or invalidated, its dependents are similarly cascaded.
|
|
571
|
+
* When an artifact rebuilds, its `factory` is executed again, re-evaluating its dependencies and state selections.
|
|
572
|
+
* `ArtifactObserverManager` is notified of changes to update any watching consumers.
|
|
573
|
+
|
|
574
|
+
### Core Concepts
|
|
575
|
+
|
|
576
|
+
* **Artifact**: A unit of work or data within the application, defined by a `key` and a `factory` function.
|
|
577
|
+
* **Scope**: Determines the lifecycle: `Singleton` (one instance) or `Transient` (new instance per resolution).
|
|
578
|
+
* **Factory**: A function that creates an artifact's instance, declaring its dependencies and side effects.
|
|
579
|
+
* **Dependencies**: Artifacts can depend on other artifacts (`resolve`/`require`) or state slices (`select`).
|
|
580
|
+
* **Reactivity**: Artifacts automatically update when their declared dependencies change.
|
|
544
581
|
|
|
545
|
-
|
|
582
|
+
---
|
|
546
583
|
|
|
547
|
-
|
|
584
|
+
## 📜 Considerations & Potential Pitfalls
|
|
548
585
|
|
|
549
|
-
|
|
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
|
-
};
|
|
586
|
+
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
587
|
|
|
560
|
-
|
|
561
|
-
initialization.do(expensiveInit),
|
|
562
|
-
initialization.do(expensiveInit),
|
|
563
|
-
initialization.do(expensiveInit),
|
|
564
|
-
]);
|
|
588
|
+
### Debouncing Latency
|
|
565
589
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
runOnceExample();
|
|
590
|
+
* **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.
|
|
591
|
+
* **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
592
|
|
|
571
|
-
|
|
572
|
-
const queue = new Serializer<string>();
|
|
573
|
-
const order: string[] = [];
|
|
593
|
+
### Wasted Build Effort from Late Staleness Detection
|
|
574
594
|
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
};
|
|
595
|
+
* **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.
|
|
596
|
+
* **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
597
|
|
|
590
|
-
|
|
591
|
-
queue.do(task1),
|
|
592
|
-
queue.do(task2),
|
|
593
|
-
queue.do(task3),
|
|
594
|
-
]);
|
|
598
|
+
### Efficiency of Global State Watching
|
|
595
599
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
+
* **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.
|
|
601
|
+
* **Recommendation**:
|
|
602
|
+
* Write granular and performant state selectors using `ctx.select()`.
|
|
603
|
+
* Minimize the number of state paths an artifact subscribes to.
|
|
604
|
+
* Ensure the `DataStore` implementation itself is optimized for change detection and notification.
|
|
600
605
|
|
|
601
|
-
|
|
606
|
+
### Startup Cost of Eager Loading
|
|
602
607
|
|
|
603
|
-
|
|
608
|
+
* **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.
|
|
609
|
+
* **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
610
|
|
|
605
|
-
|
|
611
|
+
### Sequential Cleanup/Dispose Execution
|
|
606
612
|
|
|
607
|
-
*
|
|
608
|
-
*
|
|
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.
|
|
613
|
+
* **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.
|
|
614
|
+
* **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
615
|
|
|
616
|
-
###
|
|
616
|
+
### Dependency Graph Complexity
|
|
617
617
|
|
|
618
|
-
|
|
619
|
-
|
|
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.
|
|
618
|
+
* **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.
|
|
619
|
+
* **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
620
|
|
|
647
621
|
---
|
|
648
622
|
|
|
@@ -650,8 +624,6 @@ The primary extension point is the `ArtifactFactory` function itself, which rece
|
|
|
650
624
|
|
|
651
625
|
### Development Setup
|
|
652
626
|
|
|
653
|
-
To set up the project for local development:
|
|
654
|
-
|
|
655
627
|
1. **Clone the repository:**
|
|
656
628
|
```bash
|
|
657
629
|
git clone https://github.com/asaidimu/erp-utils.git
|
|
@@ -663,44 +635,23 @@ To set up the project for local development:
|
|
|
663
635
|
# or
|
|
664
636
|
yarn install
|
|
665
637
|
```
|
|
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
638
|
|
|
671
639
|
### Scripts
|
|
672
640
|
|
|
673
|
-
The `package.json` defines the following scripts:
|
|
674
|
-
|
|
675
641
|
* `npm test`: Runs all tests once.
|
|
676
642
|
* `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.
|
|
678
643
|
|
|
679
644
|
### Testing
|
|
680
645
|
|
|
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.
|
|
646
|
+
The project uses `vitest` for testing. All new code should be accompanied by tests.
|
|
687
647
|
|
|
688
648
|
### Contributing Guidelines
|
|
689
649
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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.
|
|
650
|
+
1. **Fork and Branch**: Fork the repository and create a new branch from `main`.
|
|
651
|
+
2. **Code Quality**: Write clean, readable TypeScript code adhering to project conventions.
|
|
652
|
+
3. **Tests**: Add unit/integration tests for all new features and bug fixes.
|
|
653
|
+
4. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat: add streaming support`, `fix: correct debounce logic`).
|
|
654
|
+
5. **Pull Requests**: Submit PRs with clear descriptions and link to relevant issues.
|
|
704
655
|
|
|
705
656
|
---
|
|
706
657
|
|
|
@@ -708,33 +659,28 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
|
|
|
708
659
|
|
|
709
660
|
### Troubleshooting
|
|
710
661
|
|
|
711
|
-
* **`ArtifactNotFoundError`**:
|
|
712
|
-
* **`CircularDependencyError`**:
|
|
713
|
-
* **`TimeoutError`**:
|
|
662
|
+
* **`ArtifactNotFoundError`**: You are trying to `resolve` or `watch` an artifact that hasn't been registered. Verify artifact keys and ensure registration order.
|
|
663
|
+
* **`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.
|
|
664
|
+
* **`TimeoutError`**: An artifact factory took too long to execute. Increase the `timeout` option or optimize the factory function and its dependencies.
|
|
714
665
|
* **Unexpected Rebuilds/No Rebuilds**:
|
|
715
|
-
*
|
|
716
|
-
* Ensure
|
|
717
|
-
*
|
|
718
|
-
* Verify `onCleanup
|
|
666
|
+
* Inspect artifact status and dependencies using `container.debugInfo()`.
|
|
667
|
+
* Ensure `ctx.select()` selectors correctly capture reactive state slices.
|
|
668
|
+
* Review `debounce` settings if rebuilds are too frequent or delayed.
|
|
669
|
+
* Verify `onCleanup`/`onDispose` hooks are correctly implemented.
|
|
719
670
|
|
|
720
671
|
### FAQ
|
|
721
672
|
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* Use `resolve`
|
|
727
|
-
* Use `require` when you
|
|
728
|
-
*
|
|
729
|
-
`
|
|
730
|
-
*
|
|
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.
|
|
673
|
+
* **`Singleton` vs. `Transient` Scope**:
|
|
674
|
+
* **`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.
|
|
675
|
+
* **`Transient`**: A new instance is created every time the artifact is resolved. Useful for ephemeral objects. Does not support streaming or persistent `onDispose`.
|
|
676
|
+
* **`resolve` vs. `require`**:
|
|
677
|
+
* Use `resolve` for robust handling of artifact states (ready, error, pending) via the `ResolvedArtifact` object.
|
|
678
|
+
* Use `require` when you expect immediate success and want errors to propagate as exceptions.
|
|
679
|
+
* **`onCleanup` vs. `onDispose`**:
|
|
680
|
+
* `onCleanup`: Runs before an instance is replaced (Singleton rebuild) or discarded (Transient). Use for instance-specific resource cleanup.
|
|
681
|
+
* `onDispose`: Runs only when the artifact is permanently removed from the container. Use for global resource cleanup.
|
|
733
682
|
|
|
734
|
-
|
|
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.
|
|
683
|
+
---
|
|
738
684
|
|
|
739
685
|
### License
|
|
740
686
|
|
|
@@ -742,4 +688,4 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
|
|
|
742
688
|
|
|
743
689
|
### Acknowledgments
|
|
744
690
|
|
|
745
|
-
This library is part of the `@asaidimu/erp-utils` monorepository and
|
|
691
|
+
This library is part of the `@asaidimu/erp-utils` monorepository and relies on a compatible `DataStore` implementation for reactive state management.
|