@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 +342 -372
- package/index.d.mts +35 -34
- package/index.d.ts +35 -34
- 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)
|
|
@@ -10,23 +14,23 @@ A powerful TypeScript library for managing application components (artifacts) wi
|
|
|
10
14
|
|
|
11
15
|
## 🚀 Quick Links
|
|
12
16
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
80
|
-
import { ReactiveDataStore } from
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
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,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
|
|
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
|
+
/* ... */
|
|
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,
|
|
239
|
-
timeout: 5000,
|
|
240
|
-
retries: 3,
|
|
241
|
-
debounce: 100,
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
console.
|
|
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(
|
|
349
|
+
console.log("Service is pending or idle.");
|
|
280
350
|
}
|
|
281
351
|
|
|
282
|
-
// Using require
|
|
352
|
+
// Using require for direct access when errors are handled upstream
|
|
283
353
|
try {
|
|
284
|
-
const
|
|
285
|
-
console.log(
|
|
354
|
+
const myServiceInstance = await container.require("myService");
|
|
355
|
+
console.log("Service instance:", myServiceInstance);
|
|
286
356
|
} catch (error) {
|
|
287
|
-
|
|
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
|
|
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
|
|
366
|
+
const loggerWatcher = container.watch("logger");
|
|
301
367
|
|
|
302
|
-
const unsubscribe =
|
|
368
|
+
const unsubscribe = loggerWatcher.subscribe((resolvedArtifact) => {
|
|
303
369
|
if (resolvedArtifact.ready) {
|
|
304
|
-
console.log(
|
|
370
|
+
console.log("Logger updated:", resolvedArtifact.instance);
|
|
305
371
|
} else if (resolvedArtifact.error) {
|
|
306
|
-
console.error(
|
|
372
|
+
console.error("Logger error:", resolvedArtifact.error);
|
|
307
373
|
}
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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
|
|
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
|
|
324
|
-
|
|
388
|
+
interface AppState {
|
|
389
|
+
user: { name: string; theme: "light" | "dark" };
|
|
390
|
+
}
|
|
391
|
+
interface AppRegistry {
|
|
392
|
+
userGreeting: string;
|
|
393
|
+
}
|
|
325
394
|
|
|
326
|
-
|
|
327
|
-
const
|
|
395
|
+
// Assume appStore is an instance of DataStore<AppState>
|
|
396
|
+
const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
|
|
328
397
|
|
|
329
|
-
|
|
330
|
-
key:
|
|
398
|
+
container.register({
|
|
399
|
+
key: "userGreeting",
|
|
331
400
|
factory: async ({ use }) => {
|
|
332
|
-
// This artifact will
|
|
333
|
-
const theme = await use(({ select }) =>
|
|
334
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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:
|
|
428
|
+
key: "resourceArtifact",
|
|
361
429
|
scope: ArtifactScopes.Singleton,
|
|
362
|
-
factory: ({ onCleanup, onDispose }) => {
|
|
363
|
-
const
|
|
364
|
-
console.log(`Resource ${
|
|
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 ${
|
|
368
|
-
clearInterval(
|
|
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(
|
|
373
|
-
|
|
446
|
+
console.log(
|
|
447
|
+
`Disposing artifact 'resourceArtifact' (resource ${resourceId}).`,
|
|
448
|
+
);
|
|
449
|
+
// Final resource release
|
|
374
450
|
});
|
|
375
451
|
|
|
376
|
-
return
|
|
452
|
+
return { id: resourceId };
|
|
377
453
|
},
|
|
378
454
|
});
|
|
379
455
|
|
|
380
456
|
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.
|
|
457
|
+
await container.resolve("resourceArtifact"); // Instance created and logs "heartbeat..."
|
|
458
|
+
console.log("Artifact resolved.");
|
|
385
459
|
|
|
386
|
-
//
|
|
387
|
-
await container.
|
|
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
|
-
|
|
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:
|
|
477
|
+
key: "dataStream",
|
|
399
478
|
scope: ArtifactScopes.Singleton,
|
|
400
|
-
factory: ({ stream, onCleanup }) => {
|
|
401
|
-
let
|
|
402
|
-
let
|
|
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
|
-
|
|
405
|
-
console.log('Counter stream started...');
|
|
406
|
-
interval = setInterval(() => {
|
|
488
|
+
intervalId = setInterval(() => {
|
|
407
489
|
if (signal.aborted) {
|
|
408
|
-
console.log(
|
|
409
|
-
clearInterval(
|
|
490
|
+
console.log("Stream signal aborted. Clearing interval.");
|
|
491
|
+
clearInterval(intervalId);
|
|
410
492
|
return;
|
|
411
493
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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(
|
|
424
|
-
// Ensure interval is cleared if stream is aborted
|
|
425
|
-
clearInterval(
|
|
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
|
|
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(
|
|
517
|
+
const watcher = container.watch("dataStream");
|
|
434
518
|
const unsubscribe = watcher.subscribe((art) => {
|
|
435
|
-
if (art.ready) console.log(
|
|
519
|
+
if (art.ready) console.log("Stream received:", art.instance);
|
|
436
520
|
});
|
|
437
521
|
|
|
438
|
-
// Keep alive for a few seconds to
|
|
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(
|
|
525
|
+
console.log("Stream watcher unsubscribed.");
|
|
442
526
|
}
|
|
443
527
|
streamExample();
|
|
444
528
|
```
|
|
445
529
|
|
|
446
530
|
### Invalidating Artifacts
|
|
447
531
|
|
|
448
|
-
|
|
532
|
+
Manually trigger an artifact to rebuild and cascade invalidations to its dependents.
|
|
449
533
|
|
|
450
534
|
```typescript
|
|
451
|
-
// Invalidate
|
|
452
|
-
await container.invalidate(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
564
|
+
---
|
|
476
565
|
|
|
477
|
-
|
|
566
|
+
## 🏗️ Project Architecture
|
|
478
567
|
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
606
|
+
---
|
|
546
607
|
|
|
547
|
-
|
|
608
|
+
## 📜 Considerations & Potential Pitfalls
|
|
548
609
|
|
|
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
|
-
};
|
|
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
|
-
|
|
561
|
-
initialization.do(expensiveInit),
|
|
562
|
-
initialization.do(expensiveInit),
|
|
563
|
-
initialization.do(expensiveInit),
|
|
564
|
-
]);
|
|
612
|
+
### Debouncing Latency
|
|
565
613
|
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
const queue = new Serializer<string>();
|
|
573
|
-
const order: string[] = [];
|
|
617
|
+
### Wasted Build Effort from Late Staleness Detection
|
|
574
618
|
|
|
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
|
-
};
|
|
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
|
-
|
|
591
|
-
queue.do(task1),
|
|
592
|
-
queue.do(task2),
|
|
593
|
-
queue.do(task3),
|
|
594
|
-
]);
|
|
622
|
+
### Efficiency of Global State Watching
|
|
595
623
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
+
### Sequential Cleanup/Dispose Execution
|
|
606
636
|
|
|
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.
|
|
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
|
-
###
|
|
640
|
+
### Dependency Graph Complexity
|
|
617
641
|
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
715
|
+
This library is part of the `@asaidimu/erp-utils` monorepository and relies on a compatible `DataStore` implementation for reactive state management.
|