@asaidimu/utils-artifacts 8.0.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 +190 -166
- package/index.d.mts +10 -10
- package/index.d.ts +10 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,23 +14,23 @@ The `@asaidimu/utils-artifacts` library empowers developers to build highly orga
|
|
|
14
14
|
|
|
15
15
|
## 🚀 Quick Links
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
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)
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -40,16 +40,16 @@ The `@asaidimu/utils-artifacts` library empowers developers to build highly orga
|
|
|
40
40
|
|
|
41
41
|
### Key Features
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -59,18 +59,18 @@ The `@asaidimu/utils-artifacts` library is designed to tackle the complexities o
|
|
|
59
59
|
|
|
60
60
|
### Core Power
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
66
|
|
|
67
67
|
### Key Use Cases
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
@@ -78,9 +78,9 @@ The `@asaidimu/utils-artifacts` library is designed to tackle the complexities o
|
|
|
78
78
|
|
|
79
79
|
### Prerequisites
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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).
|
|
84
84
|
|
|
85
85
|
### Installation Steps
|
|
86
86
|
|
|
@@ -101,26 +101,26 @@ yarn add @asaidimu/utils-artifacts
|
|
|
101
101
|
You can verify the installation by attempting to import and register a basic artifact:
|
|
102
102
|
|
|
103
103
|
```typescript
|
|
104
|
-
import { ArtifactContainer } from
|
|
105
|
-
import { ReactiveDataStore } from
|
|
104
|
+
import { ArtifactContainer } from "@asaidimu/utils-artifacts";
|
|
105
|
+
import { ReactiveDataStore } from "@asaidimu/utils-store"; // Assuming you have this store
|
|
106
106
|
|
|
107
107
|
// Define your application's global state and artifact registry types
|
|
108
|
-
type AppState = { count: number
|
|
109
|
-
type AppRegistry = { myService: string
|
|
108
|
+
type AppState = { count: number };
|
|
109
|
+
type AppRegistry = { myService: string };
|
|
110
110
|
|
|
111
111
|
const store = new ReactiveDataStore<AppState>({ count: 0 });
|
|
112
112
|
const container = new ArtifactContainer<AppRegistry, AppState>(store);
|
|
113
113
|
|
|
114
114
|
container.register({
|
|
115
|
-
key:
|
|
115
|
+
key: "myService",
|
|
116
116
|
factory: async ({ use }) => {
|
|
117
|
-
const currentCount = await use(({ select }) => select(s => s.count));
|
|
117
|
+
const currentCount = await use(({ select }) => select((s) => s.count));
|
|
118
118
|
return `Service is running with count: ${currentCount}`;
|
|
119
119
|
},
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
async function runExample() {
|
|
123
|
-
const service = await container.resolve(
|
|
123
|
+
const service = await container.resolve("myService");
|
|
124
124
|
console.log(service.instance); // Expected: Service is running with count: 0
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -259,32 +259,38 @@ main();
|
|
|
259
259
|
Use `container.register()` to define an artifact. Each artifact requires a unique `key` and a `factory` function.
|
|
260
260
|
|
|
261
261
|
```typescript
|
|
262
|
-
import { ArtifactScopes, ArtifactTemplate } from
|
|
262
|
+
import { ArtifactScopes, ArtifactTemplate } from "@asaidimu/utils-artifacts";
|
|
263
263
|
|
|
264
264
|
// Define your app's state and registry types for better type safety
|
|
265
|
-
interface MyAppState {
|
|
266
|
-
|
|
265
|
+
interface MyAppState {
|
|
266
|
+
/* ... */
|
|
267
|
+
}
|
|
268
|
+
interface MyAppRegistry {
|
|
269
|
+
myService: string;
|
|
270
|
+
}
|
|
267
271
|
|
|
268
272
|
// Type assertion for the container
|
|
269
273
|
const container = new ArtifactContainer<MyAppRegistry, MyAppState>(myStore);
|
|
270
274
|
|
|
271
275
|
// Example of registering an artifact
|
|
272
|
-
container.register<MyAppRegistry, MyAppState,
|
|
273
|
-
key:
|
|
276
|
+
container.register<MyAppRegistry, MyAppState, "myService">({
|
|
277
|
+
key: "myService",
|
|
274
278
|
factory: async (ctx) => {
|
|
275
279
|
// Access dependencies using ctx.use()
|
|
276
|
-
const logger = await ctx.use(({ require }) => require(
|
|
277
|
-
const apiUrl = await ctx.use(({ select }) =>
|
|
280
|
+
const logger = await ctx.use(({ require }) => require("logger"));
|
|
281
|
+
const apiUrl = await ctx.use(({ select }) =>
|
|
282
|
+
select((state) => state.config.apiUrl),
|
|
283
|
+
);
|
|
278
284
|
|
|
279
285
|
logger.log(`Initializing myService with API URL: ${apiUrl}`);
|
|
280
286
|
return `Initialized: ${apiUrl}`;
|
|
281
287
|
},
|
|
282
288
|
// Optional parameters:
|
|
283
289
|
scope: ArtifactScopes.Singleton, // 'singleton' (default) or 'transient'
|
|
284
|
-
lazy: true,
|
|
285
|
-
timeout: 5000,
|
|
286
|
-
retries: 3,
|
|
287
|
-
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.
|
|
288
294
|
});
|
|
289
295
|
```
|
|
290
296
|
|
|
@@ -329,26 +335,26 @@ interface UseDependencyContext<TRegistry, TState> {
|
|
|
329
335
|
|
|
330
336
|
### Resolving & Requiring Artifacts
|
|
331
337
|
|
|
332
|
-
|
|
333
|
-
|
|
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.
|
|
334
340
|
|
|
335
341
|
```typescript
|
|
336
342
|
// Using resolve for defensive programming
|
|
337
|
-
const myServiceResult = await container.resolve(
|
|
343
|
+
const myServiceResult = await container.resolve("myService");
|
|
338
344
|
if (myServiceResult.ready) {
|
|
339
|
-
console.log(
|
|
345
|
+
console.log("Service instance:", myServiceResult.instance);
|
|
340
346
|
} else if (myServiceResult.error) {
|
|
341
|
-
console.error(
|
|
347
|
+
console.error("Service failed:", myServiceResult.error);
|
|
342
348
|
} else {
|
|
343
|
-
console.log(
|
|
349
|
+
console.log("Service is pending or idle.");
|
|
344
350
|
}
|
|
345
351
|
|
|
346
352
|
// Using require for direct access when errors are handled upstream
|
|
347
353
|
try {
|
|
348
|
-
const myServiceInstance = await container.require(
|
|
349
|
-
console.log(
|
|
354
|
+
const myServiceInstance = await container.require("myService");
|
|
355
|
+
console.log("Service instance:", myServiceInstance);
|
|
350
356
|
} catch (error) {
|
|
351
|
-
console.error(
|
|
357
|
+
console.error("Failed to get service:", error);
|
|
352
358
|
}
|
|
353
359
|
```
|
|
354
360
|
|
|
@@ -357,17 +363,17 @@ try {
|
|
|
357
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.
|
|
358
364
|
|
|
359
365
|
```typescript
|
|
360
|
-
const loggerWatcher = container.watch(
|
|
366
|
+
const loggerWatcher = container.watch("logger");
|
|
361
367
|
|
|
362
368
|
const unsubscribe = loggerWatcher.subscribe((resolvedArtifact) => {
|
|
363
369
|
if (resolvedArtifact.ready) {
|
|
364
|
-
console.log(
|
|
370
|
+
console.log("Logger updated:", resolvedArtifact.instance);
|
|
365
371
|
} else if (resolvedArtifact.error) {
|
|
366
|
-
console.error(
|
|
372
|
+
console.error("Logger error:", resolvedArtifact.error);
|
|
367
373
|
}
|
|
368
374
|
// `get()` retrieves the current resolved artifact state without subscribing.
|
|
369
375
|
const currentLogger = loggerWatcher.get();
|
|
370
|
-
console.log(
|
|
376
|
+
console.log("Current logger:", currentLogger.instance);
|
|
371
377
|
});
|
|
372
378
|
|
|
373
379
|
// To stop listening for updates:
|
|
@@ -380,42 +386,54 @@ Artifacts can automatically react to changes in your application's global state
|
|
|
380
386
|
|
|
381
387
|
```typescript
|
|
382
388
|
interface AppState {
|
|
383
|
-
user: { name: string; theme:
|
|
389
|
+
user: { name: string; theme: "light" | "dark" };
|
|
390
|
+
}
|
|
391
|
+
interface AppRegistry {
|
|
392
|
+
userGreeting: string;
|
|
384
393
|
}
|
|
385
|
-
interface AppRegistry { userGreeting: string; }
|
|
386
394
|
|
|
387
395
|
// Assume appStore is an instance of DataStore<AppState>
|
|
388
396
|
const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
|
|
389
397
|
|
|
390
398
|
container.register({
|
|
391
|
-
key:
|
|
399
|
+
key: "userGreeting",
|
|
392
400
|
factory: async ({ use }) => {
|
|
393
401
|
// This artifact will rebuild if appStore.user.theme changes.
|
|
394
|
-
const theme = await use(({ select }) =>
|
|
395
|
-
|
|
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
|
+
);
|
|
396
408
|
return `Hello, ${userName}! Your theme is ${theme}.`;
|
|
397
409
|
},
|
|
398
410
|
});
|
|
399
411
|
|
|
400
412
|
// Example of state change triggering rebuild
|
|
401
|
-
await appStore.set(state => ({
|
|
402
|
-
|
|
413
|
+
await appStore.set((state) => ({
|
|
414
|
+
...state,
|
|
415
|
+
user: { ...state.user, theme: "dark" },
|
|
416
|
+
}));
|
|
417
|
+
const greeting = await container.resolve("userGreeting");
|
|
403
418
|
console.log(greeting.instance); // Output will reflect the new theme.
|
|
404
419
|
```
|
|
405
420
|
|
|
406
421
|
### Artifact Lifecycle (Cleanup & Dispose)
|
|
407
422
|
|
|
408
|
-
|
|
409
|
-
|
|
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).
|
|
410
425
|
|
|
411
426
|
```typescript
|
|
412
427
|
container.register({
|
|
413
|
-
key:
|
|
428
|
+
key: "resourceArtifact",
|
|
414
429
|
scope: ArtifactScopes.Singleton,
|
|
415
430
|
factory: ({ onCleanup, onDispose, use }) => {
|
|
416
431
|
const resourceId = Math.random();
|
|
417
432
|
console.log(`Resource ${resourceId} created.`);
|
|
418
|
-
const timerId = setInterval(
|
|
433
|
+
const timerId = setInterval(
|
|
434
|
+
() => console.log(`Resource ${resourceId} heartbeat...`),
|
|
435
|
+
1000,
|
|
436
|
+
);
|
|
419
437
|
|
|
420
438
|
// Cleanup for the current instance (e.g., on rebuild or if transient)
|
|
421
439
|
onCleanup(() => {
|
|
@@ -425,7 +443,9 @@ container.register({
|
|
|
425
443
|
|
|
426
444
|
// Dispose for permanent removal from container
|
|
427
445
|
onDispose(() => {
|
|
428
|
-
console.log(
|
|
446
|
+
console.log(
|
|
447
|
+
`Disposing artifact 'resourceArtifact' (resource ${resourceId}).`,
|
|
448
|
+
);
|
|
429
449
|
// Final resource release
|
|
430
450
|
});
|
|
431
451
|
|
|
@@ -434,16 +454,16 @@ container.register({
|
|
|
434
454
|
});
|
|
435
455
|
|
|
436
456
|
async function lifecycleExample() {
|
|
437
|
-
await container.resolve(
|
|
438
|
-
console.log(
|
|
457
|
+
await container.resolve("resourceArtifact"); // Instance created and logs "heartbeat..."
|
|
458
|
+
console.log("Artifact resolved.");
|
|
439
459
|
|
|
440
460
|
// Simulate invalidation: will trigger onCleanup for the current instance, then rebuild.
|
|
441
|
-
await container.invalidate(
|
|
442
|
-
console.log(
|
|
461
|
+
await container.invalidate("resourceArtifact");
|
|
462
|
+
console.log("Artifact invalidated and rebuilt.");
|
|
443
463
|
|
|
444
464
|
// Manually dispose the artifact and container
|
|
445
|
-
await container.unregister(
|
|
446
|
-
console.log(
|
|
465
|
+
await container.unregister("resourceArtifact"); // Triggers onDispose
|
|
466
|
+
console.log("Artifact unregistered.");
|
|
447
467
|
}
|
|
448
468
|
lifecycleExample();
|
|
449
469
|
```
|
|
@@ -454,20 +474,20 @@ For artifacts that continuously emit values (e.g., real-time data, streams), use
|
|
|
454
474
|
|
|
455
475
|
```typescript
|
|
456
476
|
container.register({
|
|
457
|
-
key:
|
|
477
|
+
key: "dataStream",
|
|
458
478
|
scope: ArtifactScopes.Singleton,
|
|
459
479
|
factory: ({ stream, onCleanup, use }) => {
|
|
460
480
|
let counter = 0;
|
|
461
481
|
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
462
482
|
|
|
463
483
|
stream(async ({ emit, signal, value }) => {
|
|
464
|
-
console.log(
|
|
465
|
-
const logger = await use(({ require }) => require(
|
|
466
|
-
logger.log(
|
|
484
|
+
console.log("Data stream started...");
|
|
485
|
+
const logger = await use(({ require }) => require("logger")); // Example dependency
|
|
486
|
+
logger.log("Stream active. Emitting values.");
|
|
467
487
|
|
|
468
488
|
intervalId = setInterval(() => {
|
|
469
489
|
if (signal.aborted) {
|
|
470
|
-
console.log(
|
|
490
|
+
console.log("Stream signal aborted. Clearing interval.");
|
|
471
491
|
clearInterval(intervalId);
|
|
472
492
|
return;
|
|
473
493
|
}
|
|
@@ -476,7 +496,7 @@ container.register({
|
|
|
476
496
|
console.log(`Emitting: ${newValue}`);
|
|
477
497
|
emit(newValue); // Emit the new value to the container
|
|
478
498
|
if (counter >= 5) {
|
|
479
|
-
console.log(
|
|
499
|
+
console.log("Stream reached limit, stopping.");
|
|
480
500
|
clearInterval(intervalId);
|
|
481
501
|
// The factory function can return to stop the stream producer.
|
|
482
502
|
}
|
|
@@ -484,25 +504,25 @@ container.register({
|
|
|
484
504
|
});
|
|
485
505
|
|
|
486
506
|
onCleanup(() => {
|
|
487
|
-
console.log(
|
|
507
|
+
console.log("Cleaning up data stream instance...");
|
|
488
508
|
// Ensure interval is cleared if stream is aborted or rebuilt.
|
|
489
509
|
if (intervalId) clearInterval(intervalId);
|
|
490
510
|
});
|
|
491
511
|
|
|
492
|
-
return
|
|
512
|
+
return "Initial stream value"; // Initial value before first emission
|
|
493
513
|
},
|
|
494
514
|
});
|
|
495
515
|
|
|
496
516
|
async function streamExample() {
|
|
497
|
-
const watcher = container.watch(
|
|
517
|
+
const watcher = container.watch("dataStream");
|
|
498
518
|
const unsubscribe = watcher.subscribe((art) => {
|
|
499
|
-
if (art.ready) console.log(
|
|
519
|
+
if (art.ready) console.log("Stream received:", art.instance);
|
|
500
520
|
});
|
|
501
521
|
|
|
502
522
|
// Keep alive for a few seconds to observe stream emissions
|
|
503
|
-
await new Promise(res => setTimeout(res, 3000));
|
|
523
|
+
await new Promise((res) => setTimeout(res, 3000));
|
|
504
524
|
unsubscribe();
|
|
505
|
-
console.log(
|
|
525
|
+
console.log("Stream watcher unsubscribed.");
|
|
506
526
|
}
|
|
507
527
|
streamExample();
|
|
508
528
|
```
|
|
@@ -513,10 +533,10 @@ Manually trigger an artifact to rebuild and cascade invalidations to its depende
|
|
|
513
533
|
|
|
514
534
|
```typescript
|
|
515
535
|
// Invalidate an artifact; it will rebuild if it has dependents, is lazy and watched, or eagerly.
|
|
516
|
-
await container.invalidate(
|
|
536
|
+
await container.invalidate("myArtifact");
|
|
517
537
|
|
|
518
538
|
// Force immediate rebuild, bypassing any debounce delay.
|
|
519
|
-
await container.invalidate(
|
|
539
|
+
await container.invalidate("myArtifact", true);
|
|
520
540
|
```
|
|
521
541
|
|
|
522
542
|
### Debugging Artifacts
|
|
@@ -525,14 +545,18 @@ The `container.debugInfo()` method provides a snapshot of the container's intern
|
|
|
525
545
|
|
|
526
546
|
```typescript
|
|
527
547
|
const debugNodes = container.debugInfo();
|
|
528
|
-
debugNodes.forEach(node => {
|
|
548
|
+
debugNodes.forEach((node) => {
|
|
529
549
|
console.log(`
|
|
530
550
|
Artifact ID: ${node.id}`);
|
|
531
551
|
console.log(` Scope: ${node.scope}`);
|
|
532
552
|
console.log(` Status: ${node.status}`); // e.g., 'active', 'error', 'idle', 'building', 'pending', 'debouncing'
|
|
533
|
-
console.log(
|
|
534
|
-
|
|
535
|
-
|
|
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"}`);
|
|
536
560
|
console.log(` Build Count: ${node.buildCount}`);
|
|
537
561
|
});
|
|
538
562
|
```
|
|
@@ -543,41 +567,41 @@ Artifact ID: ${node.id}`);
|
|
|
543
567
|
|
|
544
568
|
The `@asaidimu/utils-artifacts` library is architected around a central `ArtifactContainer` that orchestrates several specialized internal components:
|
|
545
569
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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.
|
|
552
576
|
|
|
553
577
|
### Data Flow Example (Resolution & Reactivity)
|
|
554
578
|
|
|
555
579
|
1. **Registration**: An artifact (`key`, `factory`, `scope`, etc.) is registered with the `ArtifactRegistry`. The `ArtifactDependencyGraph` registers a node for this artifact.
|
|
556
580
|
2. **Resolution (`container.resolve`)**:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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.
|
|
566
590
|
3. **Reactivity & Invalidation**:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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.
|
|
573
597
|
|
|
574
598
|
### Core Concepts
|
|
575
599
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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.
|
|
581
605
|
|
|
582
606
|
---
|
|
583
607
|
|
|
@@ -587,36 +611,36 @@ While `@asaidimu/utils-artifacts` offers powerful features, understanding its op
|
|
|
587
611
|
|
|
588
612
|
### Debouncing Latency
|
|
589
613
|
|
|
590
|
-
|
|
591
|
-
|
|
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.
|
|
592
616
|
|
|
593
617
|
### Wasted Build Effort from Late Staleness Detection
|
|
594
618
|
|
|
595
|
-
|
|
596
|
-
|
|
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.
|
|
597
621
|
|
|
598
622
|
### Efficiency of Global State Watching
|
|
599
623
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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.
|
|
605
629
|
|
|
606
630
|
### Startup Cost of Eager Loading
|
|
607
631
|
|
|
608
|
-
|
|
609
|
-
|
|
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.
|
|
610
634
|
|
|
611
635
|
### Sequential Cleanup/Dispose Execution
|
|
612
636
|
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
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.
|
|
620
644
|
|
|
621
645
|
---
|
|
622
646
|
|
|
@@ -638,8 +662,8 @@ While `@asaidimu/utils-artifacts` offers powerful features, understanding its op
|
|
|
638
662
|
|
|
639
663
|
### Scripts
|
|
640
664
|
|
|
641
|
-
|
|
642
|
-
|
|
665
|
+
- `npm test`: Runs all tests once.
|
|
666
|
+
- `npm test:watch`: Runs tests in watch mode, rerunning on file changes.
|
|
643
667
|
|
|
644
668
|
### Testing
|
|
645
669
|
|
|
@@ -659,26 +683,26 @@ The project uses `vitest` for testing. All new code should be accompanied by tes
|
|
|
659
683
|
|
|
660
684
|
### Troubleshooting
|
|
661
685
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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.
|
|
670
694
|
|
|
671
695
|
### FAQ
|
|
672
696
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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.
|
|
682
706
|
|
|
683
707
|
---
|
|
684
708
|
|
package/index.d.mts
CHANGED
|
@@ -364,17 +364,17 @@ interface ArtifactFactoryContext<TRegistry extends Record<string, any>, TState e
|
|
|
364
364
|
*/
|
|
365
365
|
onDispose(callback: ArtifactCleanup): void;
|
|
366
366
|
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
367
|
+
* Starts a streaming process for a Singleton artifact.
|
|
368
|
+
* The callback receives an `ArtifactStreamContext` to emit values and manage the
|
|
369
|
+
* stream's lifecycle. It can return a cleanup function (or a Promise resolving
|
|
370
|
+
* to one) to handle resource disposal.
|
|
371
|
+
* * @param callback - The streaming logic implementation. Can be synchronous or
|
|
372
|
+
* asynchronous, optionally returning a cleanup function.
|
|
373
|
+
* @throws {SystemError} If called on a Transient artifact, as they do not
|
|
374
|
+
* support persistent streaming states.
|
|
375
|
+
*/
|
|
376
|
+
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => (void | (() => void | Promise<void>)) | Promise<void | (() => void | Promise<void>)>): void;
|
|
376
377
|
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => (void | (() => void | Promise<void>)) | Promise<void | (() => void | Promise<void>)>): void;
|
|
377
|
-
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => (void | (() => (void | Promise<void>))) | Promise<(void | (() => (void | Promise<void>)))>): void;
|
|
378
378
|
/**
|
|
379
379
|
* An AbortSignal that indicates if the artifacts has been unregistered
|
|
380
380
|
*/
|
package/index.d.ts
CHANGED
|
@@ -364,17 +364,17 @@ interface ArtifactFactoryContext<TRegistry extends Record<string, any>, TState e
|
|
|
364
364
|
*/
|
|
365
365
|
onDispose(callback: ArtifactCleanup): void;
|
|
366
366
|
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
367
|
+
* Starts a streaming process for a Singleton artifact.
|
|
368
|
+
* The callback receives an `ArtifactStreamContext` to emit values and manage the
|
|
369
|
+
* stream's lifecycle. It can return a cleanup function (or a Promise resolving
|
|
370
|
+
* to one) to handle resource disposal.
|
|
371
|
+
* * @param callback - The streaming logic implementation. Can be synchronous or
|
|
372
|
+
* asynchronous, optionally returning a cleanup function.
|
|
373
|
+
* @throws {SystemError} If called on a Transient artifact, as they do not
|
|
374
|
+
* support persistent streaming states.
|
|
375
|
+
*/
|
|
376
|
+
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => (void | (() => void | Promise<void>)) | Promise<void | (() => void | Promise<void>)>): void;
|
|
376
377
|
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => (void | (() => void | Promise<void>)) | Promise<void | (() => void | Promise<void>)>): void;
|
|
377
|
-
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => (void | (() => (void | Promise<void>))) | Promise<(void | (() => (void | Promise<void>)))>): void;
|
|
378
378
|
/**
|
|
379
379
|
* An AbortSignal that indicates if the artifacts has been unregistered
|
|
380
380
|
*/
|