@asaidimu/utils-artifacts 8.2.4 â 8.2.6
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 +228 -596
- package/index.d.mts +47 -4
- package/index.d.ts +47 -4
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,715 +1,347 @@
|
|
|
1
1
|
# @asaidimu/utils-artifacts
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[
|
|
10
|
-
[
|
|
11
|
-
[
|
|
3
|
+
Reactive dependency injection and lifecycle management for TypeScript applications.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Overview](#overview)
|
|
10
|
+
2. [Core Concepts](#core-concepts)
|
|
11
|
+
3. [Quick Start](#quick-start)
|
|
12
|
+
4. [Usage](#usage)
|
|
13
|
+
- [Registering Artifacts](#registering-artifacts)
|
|
14
|
+
- [Resolving Dependencies](# resolves-dependencies)
|
|
15
|
+
- [Streaming Artifacts](#streaming-artifacts)
|
|
16
|
+
- [Parameterized Artifacts](#parameterized-artifacts)
|
|
17
|
+
- [Lifecycle Management](#lifecycle-management)
|
|
18
|
+
- [Observing Artifacts](#observing-artifacts)
|
|
19
|
+
5. [Architecture](#architecture)
|
|
20
|
+
6. [Development](#development)
|
|
21
|
+
7. [Testing](#testing)
|
|
22
|
+
8. [API Reference](#api-reference)
|
|
12
23
|
|
|
13
24
|
---
|
|
14
25
|
|
|
15
|
-
##
|
|
16
|
-
|
|
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
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## ⨠Overview & Features
|
|
38
|
-
|
|
39
|
-
`@asaidimu/utils-artifacts` is a reactive dependency injection container designed to bring order and efficiency to complex application architectures. It allows you to define application components (called "artifacts") as factories that declare their dependencies on other artifacts and global application state. The container automatically manages the lifecycle, instantiation, and invalidation of these artifacts, ensuring that components are always up-to-date with their dependencies.
|
|
40
|
-
|
|
41
|
-
### Key Features
|
|
42
|
-
|
|
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.
|
|
26
|
+
## 1. Overview
|
|
74
27
|
|
|
75
|
-
|
|
28
|
+
`@asaidimu/utils-artifacts` is a high-performance TypeScript library for managing the lifecycle, dependencies, and reactivity of application components, referred to as **artifacts**.
|
|
76
29
|
|
|
77
|
-
|
|
30
|
+
Unlike traditional Dependency Injection (DI) containers, this system implements a **Lazy-Resolution, Lazy-Invalidation** model. This ensures that components are created only when requested and are rebuilt only when their specific dependencies change, minimizing unnecessary re-computations and avoiding the manual orchestration of event listeners in the UI layer.
|
|
78
31
|
|
|
79
|
-
|
|
32
|
+
## 2. Core Concepts
|
|
80
33
|
|
|
81
|
-
|
|
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).
|
|
34
|
+
To use the library effectively, it is essential to understand the mental model of an artifact's lifecycle:
|
|
84
35
|
|
|
85
|
-
###
|
|
36
|
+
### The Artifact Lifecycle
|
|
86
37
|
|
|
87
|
-
|
|
38
|
+
An artifact transitions through the following states:
|
|
88
39
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```
|
|
40
|
+
- **Idle**: The artifact is registered but has not yet been resolved. No resources are consumed.
|
|
41
|
+
- **Pending**: The factory function is currently executing (asynchronous resolution).
|
|
42
|
+
- **Ready**: The artifact instance is available and cached.
|
|
43
|
+
- **Invalidated**: A dependency change has occurred. The cached instance is marked as stale, but the artifact is not rebuilt immediately. It returns to the **Idle** state for the next `resolve()` call.
|
|
94
44
|
|
|
95
|
-
###
|
|
45
|
+
### Singleton vs. Transient
|
|
96
46
|
|
|
97
|
-
|
|
47
|
+
- **Singleton**: A single instance is shared across the entire container. Ideal for services, API clients, and shared state managers.
|
|
48
|
+
- **Transient**: A fresh instance is created on every `resolve()` call. Ideal for lightweight utility functions or state transformations.
|
|
98
49
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
You can verify the installation by attempting to import and register a basic artifact:
|
|
50
|
+
## 3. Quick Start
|
|
102
51
|
|
|
103
52
|
```typescript
|
|
53
|
+
import { ReactiveDataStore } from "@asaidimu/utils-store";
|
|
104
54
|
import { ArtifactContainer } from "@asaidimu/utils-artifacts";
|
|
105
|
-
import { ReactiveDataStore } from "@asaidimu/utils-store"; // Assuming you have this store
|
|
106
|
-
|
|
107
|
-
// Define your application's global state and artifact registry types
|
|
108
|
-
type AppState = { count: number };
|
|
109
|
-
type AppRegistry = { myService: string };
|
|
110
55
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
container.register({
|
|
115
|
-
key: "myService",
|
|
116
|
-
factory: async ({ use }) => {
|
|
117
|
-
const currentCount = await use(({ select }) => select((s) => s.count));
|
|
118
|
-
return `Service is running with count: ${currentCount}`;
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
async function runExample() {
|
|
123
|
-
const service = await container.resolve("myService");
|
|
124
|
-
console.log(service.instance); // Expected: Service is running with count: 0
|
|
56
|
+
interface State {
|
|
57
|
+
count: number;
|
|
125
58
|
}
|
|
126
59
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## đ Usage Documentation
|
|
133
|
-
|
|
134
|
-
### Basic Usage
|
|
135
|
-
|
|
136
|
-
The core of the library is the `ArtifactContainer`. You initialize it with a `DataStore` that manages your application's global state.
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
|
|
140
|
-
import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming this is your state store
|
|
141
|
-
|
|
142
|
-
// 1. Define your application's global state and artifact registry types
|
|
143
|
-
interface AppState {
|
|
144
|
-
appName: string;
|
|
145
|
-
version: string;
|
|
146
|
-
config: {
|
|
147
|
-
theme: 'light' | 'dark';
|
|
148
|
-
apiUrl: string;
|
|
149
|
-
};
|
|
60
|
+
interface Registry {
|
|
61
|
+
counterLabel: string;
|
|
150
62
|
}
|
|
151
63
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
apiClient: ApiClient;
|
|
155
|
-
themeService: 'light' | 'dark';
|
|
156
|
-
appInfo: string;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Example DataStore (from @asaidimu/utils-store or similar)
|
|
160
|
-
const appStore = new ReactiveDataStore<AppState>({
|
|
161
|
-
appName: 'My App',
|
|
162
|
-
version: '1.0.0',
|
|
163
|
-
config: {
|
|
164
|
-
theme: 'light',
|
|
165
|
-
apiUrl: 'https://api.example.com',
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// 2. Instantiate the ArtifactContainer
|
|
170
|
-
const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
|
|
171
|
-
|
|
172
|
-
// Mock services for demonstration
|
|
173
|
-
class LoggerService {
|
|
174
|
-
constructor(private prefix: string) {}
|
|
175
|
-
log(message: string) {
|
|
176
|
-
console.log(`[${this.prefix}] ${message}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
interface ApiClient {
|
|
181
|
-
fetchData(path: string): Promise<{ message: string }>;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 3. Define and register your artifact factories
|
|
185
|
-
container.register({
|
|
186
|
-
key: 'logger',
|
|
187
|
-
scope: ArtifactScopes.Singleton, // Ensure only one instance of the logger
|
|
188
|
-
factory: () => new LoggerService('APP'),
|
|
189
|
-
});
|
|
64
|
+
const store = new ReactiveDataStore<State>({ count: 0 });
|
|
65
|
+
const container = new ArtifactContainer<Registry, State>(store);
|
|
190
66
|
|
|
67
|
+
// Register a reactive artifact
|
|
191
68
|
container.register({
|
|
192
|
-
key:
|
|
193
|
-
scope: ArtifactScopes.Singleton,
|
|
69
|
+
key: "counterLabel",
|
|
194
70
|
factory: async ({ use }) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const apiUrl = await use(({ select }) => select(state => state.config.apiUrl));
|
|
198
|
-
|
|
199
|
-
logger.log(`Initializing API client for ${apiUrl}`);
|
|
200
|
-
return {
|
|
201
|
-
fetchData: async (path: string) => {
|
|
202
|
-
logger.log(`Fetching from ${apiUrl}${path}`);
|
|
203
|
-
// Simulate API call
|
|
204
|
-
await new Promise(res => setTimeout(res, 100));
|
|
205
|
-
return { message: `Data from ${apiUrl}${path}` };
|
|
206
|
-
}
|
|
207
|
-
};
|
|
71
|
+
const count = await use((ctx) => ctx.select((s) => s.count));
|
|
72
|
+
return `The current count is ${count}`;
|
|
208
73
|
},
|
|
209
74
|
});
|
|
210
75
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
factory: async ({ use }) => {
|
|
215
|
-
// Theme service depends directly on state
|
|
216
|
-
return await use(({ select }) => select(state => state.config.theme));
|
|
217
|
-
},
|
|
218
|
-
});
|
|
76
|
+
// Resolve the artifact
|
|
77
|
+
const result = await container.resolve("counterLabel");
|
|
78
|
+
console.log(result.instance); // "The current count is 0"
|
|
219
79
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// App info depends on other artifacts and state
|
|
225
|
-
const logger = await use(({ require }) => require('logger'));
|
|
226
|
-
const apiClient = await use(({ require }) => require('apiClient'));
|
|
227
|
-
const appName = await use(({ select }) => select(s => s.appName));
|
|
228
|
-
|
|
229
|
-
const data = await apiClient.fetchData('/info');
|
|
230
|
-
logger.log(`App Info: ${appName}, Data: ${data.message}`);
|
|
231
|
-
return `App: ${appName}, API Data: ${data.message}`;
|
|
232
|
-
},
|
|
80
|
+
// watch the artifact
|
|
81
|
+
const observer = container.watch("counterLabel");
|
|
82
|
+
observer.subscribe((result) => {
|
|
83
|
+
console.log(`Observed: ${result.instance}`); // "The current count is 0"
|
|
233
84
|
});
|
|
234
85
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(appInfo.instance); // Logs the app information after API call
|
|
242
|
-
|
|
243
|
-
// Example of reacting to state changes
|
|
244
|
-
console.log('
|
|
245
|
-
--- Changing theme ---');
|
|
246
|
-
await appStore.set(s => ({ ...s, config: { ...s.config, theme: 'dark' } }));
|
|
86
|
+
// simulate state changes
|
|
87
|
+
while (true) {
|
|
88
|
+
const { count } = store.get();
|
|
89
|
+
if (count === 10) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
247
92
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const newTheme = await container.resolve('themeService');
|
|
251
|
-
console.log('New Theme:', newTheme.instance); // Expected: dark
|
|
93
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
94
|
+
store.set(({ count }) => ({ count: count + 1 }));
|
|
252
95
|
}
|
|
253
96
|
|
|
254
|
-
|
|
97
|
+
// TODO
|
|
98
|
+
// add example operations for invalidate and calling cleanup
|
|
99
|
+
// add more examples for complex artifact interactions
|
|
255
100
|
```
|
|
256
101
|
|
|
102
|
+
## 4. Usage
|
|
103
|
+
|
|
257
104
|
### Registering Artifacts
|
|
258
105
|
|
|
259
|
-
|
|
106
|
+
Artifacts are registered via an `ArtifactTemplate`. By default, artifacts are singletons and lazily instantiated.
|
|
260
107
|
|
|
261
108
|
```typescript
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
},
|
|
288
|
-
// Optional parameters:
|
|
289
|
-
scope: ArtifactScopes.Singleton, // 'singleton' (default) or 'transient'
|
|
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.
|
|
109
|
+
container.register({
|
|
110
|
+
key: "apiClient",
|
|
111
|
+
factory: () => new ApiClient(),
|
|
112
|
+
scope: "singleton",
|
|
113
|
+
lazy: true,
|
|
294
114
|
});
|
|
295
115
|
```
|
|
296
116
|
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
*/
|
|
306
|
-
interface ArtifactFactoryContext<TRegistry, TState, TArtifact> {
|
|
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. */
|
|
312
|
-
use<R>(callback: (ctx: UseDependencyContext<TRegistry, TState>) => R | Promise<R>): Promise<R>;
|
|
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;
|
|
319
|
-
}
|
|
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
|
-
*/
|
|
326
|
-
interface UseDependencyContext<TRegistry, TState> {
|
|
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;
|
|
333
|
-
}
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### Resolving & Requiring Artifacts
|
|
337
|
-
|
|
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.
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
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);
|
|
348
|
-
} else {
|
|
349
|
-
console.log("Service is pending or idle.");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Using require for direct access when errors are handled upstream
|
|
353
|
-
try {
|
|
354
|
-
const myServiceInstance = await container.require("myService");
|
|
355
|
-
console.log("Service instance:", myServiceInstance);
|
|
356
|
-
} catch (error) {
|
|
357
|
-
console.error("Failed to get service:", error);
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Watching Artifact Changes
|
|
117
|
+
### Resolving Dependencies
|
|
362
118
|
|
|
363
|
-
|
|
119
|
+
Use the `ctx.use` callback to declare dependencies on other artifacts or state slices. This is the mandatory method for the container to track the dependency graph and trigger invalidations.
|
|
364
120
|
|
|
365
121
|
```typescript
|
|
366
|
-
|
|
122
|
+
container.register({
|
|
123
|
+
key: "userService",
|
|
124
|
+
factory: async ({ use }) => {
|
|
125
|
+
// Dependency on another artifact
|
|
126
|
+
const client = await use((ctx) => ctx.require("apiClient"));
|
|
127
|
+
// Dependency on a slice of the global state
|
|
128
|
+
const config = await use((ctx) => ctx.select((s) => s.authConfig));
|
|
367
129
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log("Logger updated:", resolvedArtifact.instance);
|
|
371
|
-
} else if (resolvedArtifact.error) {
|
|
372
|
-
console.error("Logger error:", resolvedArtifact.error);
|
|
373
|
-
}
|
|
374
|
-
// `get()` retrieves the current resolved artifact state without subscribing.
|
|
375
|
-
const currentLogger = loggerWatcher.get();
|
|
376
|
-
console.log("Current logger:", currentLogger.instance);
|
|
130
|
+
return new UserService(client, config);
|
|
131
|
+
},
|
|
377
132
|
});
|
|
378
|
-
|
|
379
|
-
// To stop listening for updates:
|
|
380
|
-
// unsubscribe();
|
|
381
133
|
```
|
|
382
134
|
|
|
383
|
-
###
|
|
135
|
+
### Streaming Artifacts
|
|
384
136
|
|
|
385
|
-
|
|
137
|
+
Singletons can emit multiple values over time. The container automatically notifies all dependents and observers upon every emission, triggering a cascading invalidation.
|
|
386
138
|
|
|
387
139
|
```typescript
|
|
388
|
-
interface AppState {
|
|
389
|
-
user: { name: string; theme: "light" | "dark" };
|
|
390
|
-
}
|
|
391
|
-
interface AppRegistry {
|
|
392
|
-
userGreeting: string;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Assume appStore is an instance of DataStore<AppState>
|
|
396
|
-
const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
|
|
397
|
-
|
|
398
140
|
container.register({
|
|
399
|
-
key: "
|
|
400
|
-
factory:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
141
|
+
key: "socketData",
|
|
142
|
+
factory: ({ stream }) => {
|
|
143
|
+
stream(({ emit, signal }) => {
|
|
144
|
+
const socket = new WebSocket("...");
|
|
145
|
+
socket.onmessage = (e) => emit(e.data);
|
|
146
|
+
|
|
147
|
+
// IMPORTANT: Always use the signal to stop the stream
|
|
148
|
+
// when the artifact is invalidated or unregistered.
|
|
149
|
+
signal.addEventListener("abort", () => {
|
|
150
|
+
socket.close();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// If the stream is a loop, check signal.aborted in every iteration
|
|
154
|
+
// while (!signal.aborted) {
|
|
155
|
+
// await someAsyncWork();
|
|
156
|
+
// emit(result);
|
|
157
|
+
// }
|
|
158
|
+
});
|
|
159
|
+
return null; // Initial value
|
|
409
160
|
},
|
|
410
161
|
});
|
|
411
|
-
|
|
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.
|
|
419
162
|
```
|
|
420
163
|
|
|
421
|
-
###
|
|
164
|
+
### Parameterized Artifacts
|
|
422
165
|
|
|
423
|
-
|
|
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).
|
|
166
|
+
Define a `paramKey` to create distinct instances based on resolution parameters.
|
|
425
167
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
() => console.log(`Resource ${resourceId} heartbeat...`),
|
|
435
|
-
1000,
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
// Cleanup for the current instance (e.g., on rebuild or if transient)
|
|
439
|
-
onCleanup(() => {
|
|
440
|
-
console.log(`Cleaning up instance for resource ${resourceId}...`);
|
|
441
|
-
clearInterval(timerId);
|
|
442
|
-
});
|
|
168
|
+
> **Critical Resource Management**
|
|
169
|
+
> Parameterized singletons are cached indefinitely by default to ensure reactive consistency. In high-cardinality scenarios (e.g., resolving thousands of unique user IDs), this can lead to memory exhaustion over long application runtimes.
|
|
170
|
+
>
|
|
171
|
+
> **Best Practices for Resource Management**:
|
|
172
|
+
>
|
|
173
|
+
> 1. **Prefer `transient` scope**: If the artifact does not need to be shared across multiple consumers, use `scope: 'transient'` to avoid cache accumulation.
|
|
174
|
+
> 2. **Manual Cleanup**: For `singleton` parameterized artifacts, invoke `container.unregister(key, params)` when the artifact is no longer needed (e.g., when a user logs out or a view is destroyed).
|
|
175
|
+
> 3. **Monitor Cache**: If resolving dynamic, high-volume data, track the number of active parameter keys to ensure the container remains within memory constraints.
|
|
443
176
|
|
|
444
|
-
|
|
445
|
-
onDispose(() => {
|
|
446
|
-
console.log(
|
|
447
|
-
`Disposing artifact 'resourceArtifact' (resource ${resourceId}).`,
|
|
448
|
-
);
|
|
449
|
-
// Final resource release
|
|
450
|
-
});
|
|
177
|
+
**Critical Note**: The `paramKey` function must be **deterministic**. The string returned by this function is the actual primary key used for caching and invalidation. If the function is non-deterministic or produces collisions, you will either lose caching benefits or accidentally invalidate unrelated instances of the same parameterized artifact.
|
|
451
178
|
|
|
452
|
-
|
|
179
|
+
```typescript
|
|
180
|
+
container.register({
|
|
181
|
+
key: "userProfile",
|
|
182
|
+
paramKey: (params) => `user:${params.id}`,
|
|
183
|
+
factory: async (ctx) => {
|
|
184
|
+
const { id } = ctx.params;
|
|
185
|
+
return fetchUser(id);
|
|
453
186
|
},
|
|
454
187
|
});
|
|
455
188
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
console.log("Artifact resolved.");
|
|
459
|
-
|
|
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.");
|
|
467
|
-
}
|
|
468
|
-
lifecycleExample();
|
|
189
|
+
// Resolves the cached singleton for user:123
|
|
190
|
+
const user = await container.resolve("userProfile", { id: "123" });
|
|
469
191
|
```
|
|
470
192
|
|
|
471
|
-
###
|
|
193
|
+
### Lifecycle Management
|
|
472
194
|
|
|
473
|
-
|
|
195
|
+
Explicitly manage the lifecycle of an artifact by using `onCleanup` and `onDispose` within the factory.
|
|
474
196
|
|
|
475
197
|
```typescript
|
|
476
198
|
container.register({
|
|
477
|
-
key: "
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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.");
|
|
487
|
-
|
|
488
|
-
intervalId = setInterval(() => {
|
|
489
|
-
if (signal.aborted) {
|
|
490
|
-
console.log("Stream signal aborted. Clearing interval.");
|
|
491
|
-
clearInterval(intervalId);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
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.
|
|
502
|
-
}
|
|
503
|
-
}, 500);
|
|
504
|
-
});
|
|
199
|
+
key: "connection",
|
|
200
|
+
factory: async ({ onCleanup, onDispose }) => {
|
|
201
|
+
const conn = new Connection();
|
|
202
|
+
await conn.connect();
|
|
505
203
|
|
|
506
|
-
onCleanup(() =>
|
|
507
|
-
|
|
508
|
-
// Ensure interval is cleared if stream is aborted or rebuilt.
|
|
509
|
-
if (intervalId) clearInterval(intervalId);
|
|
510
|
-
});
|
|
204
|
+
onCleanup(() => conn.disconnect());
|
|
205
|
+
onDispose(() => conn.destroy());
|
|
511
206
|
|
|
512
|
-
return
|
|
207
|
+
return conn;
|
|
513
208
|
},
|
|
514
209
|
});
|
|
515
|
-
|
|
516
|
-
async function streamExample() {
|
|
517
|
-
const watcher = container.watch("dataStream");
|
|
518
|
-
const unsubscribe = watcher.subscribe((art) => {
|
|
519
|
-
if (art.ready) console.log("Stream received:", art.instance);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// Keep alive for a few seconds to observe stream emissions
|
|
523
|
-
await new Promise((res) => setTimeout(res, 3000));
|
|
524
|
-
unsubscribe();
|
|
525
|
-
console.log("Stream watcher unsubscribed.");
|
|
526
|
-
}
|
|
527
|
-
streamExample();
|
|
528
210
|
```
|
|
529
211
|
|
|
530
|
-
###
|
|
212
|
+
### Observing Artifacts
|
|
531
213
|
|
|
532
|
-
|
|
214
|
+
Use `ArtifactObserver` (via `container.watch`) to bind the artifact's state to UI components or external monitors.
|
|
533
215
|
|
|
534
216
|
```typescript
|
|
535
|
-
|
|
536
|
-
await container.invalidate("myArtifact");
|
|
537
|
-
|
|
538
|
-
// Force immediate rebuild, bypassing any debounce delay.
|
|
539
|
-
await container.invalidate("myArtifact", true);
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
### Debugging Artifacts
|
|
543
|
-
|
|
544
|
-
The `container.debugInfo()` method provides a snapshot of the container's internal state, which is invaluable for understanding dependencies, status, and build counts.
|
|
217
|
+
const observer = container.watch("userProfile", { id: "123" });
|
|
545
218
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
console.log(` Scope: ${node.scope}`);
|
|
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"}`);
|
|
560
|
-
console.log(` Build Count: ${node.buildCount}`);
|
|
219
|
+
observer.subscribe((resolved) => {
|
|
220
|
+
console.log("Artifact state changed:", resolved.ready);
|
|
221
|
+
if (resolved.ready) {
|
|
222
|
+
console.log("Instance available:", resolved.instance);
|
|
223
|
+
}
|
|
561
224
|
});
|
|
562
225
|
```
|
|
563
226
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
## đī¸ Project Architecture
|
|
567
|
-
|
|
568
|
-
The `@asaidimu/utils-artifacts` library is architected around a central `ArtifactContainer` that orchestrates several specialized internal components:
|
|
569
|
-
|
|
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.
|
|
576
|
-
|
|
577
|
-
### Data Flow Example (Resolution & Reactivity)
|
|
578
|
-
|
|
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.
|
|
227
|
+
## 5. Architecture
|
|
605
228
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
## đ Considerations & Potential Pitfalls
|
|
229
|
+
`@asaidimu/utils-artifacts` implements a **Directed Acyclic Graph (DAG)** for dependency management using a **Pull-based Invalidation** model.
|
|
609
230
|
|
|
610
|
-
|
|
231
|
+
### Troubleshooting Circular Dependencies
|
|
611
232
|
|
|
612
|
-
|
|
233
|
+
Circular dependencies occur when artifact A depends on B, and B depends on A, creating a cycle in the DAG. The container will detect this at runtime and throw a `SystemError`.
|
|
613
234
|
|
|
614
|
-
|
|
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.
|
|
235
|
+
**Common Remediation Strategies**:
|
|
616
236
|
|
|
617
|
-
|
|
237
|
+
1. **Extract Shared Logic**: Identify the common dependency shared by both artifacts and move it into a third, independent base artifact.
|
|
238
|
+
2. **Use Provider Pattern**: Refactor the shared behavior into a 'provider' artifact that both A and B can depend on without forming a loop.
|
|
239
|
+
3. **Decompose Artifacts**: Break the circular logic into smaller, discrete units where one unit coordinates the interaction between the others.
|
|
618
240
|
|
|
619
|
-
|
|
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.
|
|
241
|
+
### Push vs. Pull Reactivity
|
|
621
242
|
|
|
622
|
-
|
|
243
|
+
In traditional reactive systems (Push model), a change in a source value immediately triggers a chain reaction of re-computations across all dependents. This often leads to "update storms" where a single state change causes dozens of immediate, synchronous re-renders or calculations, regardless of whether the result is actually needed at that moment.
|
|
623
244
|
|
|
624
|
-
|
|
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.
|
|
245
|
+
In contrast, this library uses a **Pull model**. When a state slice or source artifact emits a change, the container simply marks all downstream dependent artifacts as **Invalidated** (setting a stale flag). It does not execute any factories. No work is performed until a consumer actually calls `resolve()` or an observer is notified.
|
|
629
246
|
|
|
630
|
-
|
|
247
|
+
This approach ensures that if a state value changes ten times in a single event loop, the dependent artifacts are only rebuilt **once**âthe next time they are requested. This collapses multiple invalidations into a single, deferred rebuild, maximizing computational efficiency and reducing main-thread blocking.
|
|
631
248
|
|
|
632
|
-
|
|
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.
|
|
249
|
+
## 6. Development
|
|
634
250
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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.
|
|
251
|
+
```bash
|
|
252
|
+
# Install dependencies
|
|
253
|
+
bun install
|
|
639
254
|
|
|
640
|
-
|
|
255
|
+
# Run unit tests
|
|
256
|
+
bun test
|
|
641
257
|
|
|
642
|
-
|
|
643
|
-
|
|
258
|
+
# Run tests in watch mode
|
|
259
|
+
bun run test:watch
|
|
260
|
+
```
|
|
644
261
|
|
|
645
|
-
|
|
262
|
+
## 7. Testing
|
|
646
263
|
|
|
647
|
-
|
|
264
|
+
The library is verified against a comprehensive suite including:
|
|
648
265
|
|
|
649
|
-
|
|
266
|
+
- Resolution race condition prevention.
|
|
267
|
+
- Circular dependency detection.
|
|
268
|
+
- Circular dependency detection.
|
|
269
|
+
- Stream lifecycle and AbortSignal compliance.
|
|
270
|
+
- Parameterized invalidation cascades.
|
|
650
271
|
|
|
651
|
-
|
|
652
|
-
```bash
|
|
653
|
-
git clone https://github.com/asaidimu/erp-utils.git
|
|
654
|
-
cd erp-utils/src/artifacts
|
|
655
|
-
```
|
|
656
|
-
2. **Install dependencies:**
|
|
657
|
-
```bash
|
|
658
|
-
npm install
|
|
659
|
-
# or
|
|
660
|
-
yarn install
|
|
661
|
-
```
|
|
272
|
+
## 8. API Reference
|
|
662
273
|
|
|
663
|
-
###
|
|
274
|
+
### ArtifactContainer
|
|
664
275
|
|
|
665
|
-
|
|
666
|
-
|
|
276
|
+
| Method | Description |
|
|
277
|
+
| :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
278
|
+
| `register(template)` | Registers a new artifact template. |
|
|
279
|
+
| `resolve(key, params?)` | Resolves the artifact. Returns a `ResolvedArtifact` object containing the `instance` and `ready` status. |
|
|
280
|
+
| `require(key, params?)` | Same as `resolve`, but returns the instance directly or throws if resolution if resolution fails. |
|
|
281
|
+
| `invalidate(key, params?)` | Manually trigger a rebuild of the artifact and its dependents. |
|
|
282
|
+
| `unregister(key, params?)` | Removes the artifact from the registry. If `params` is omitted, the entire template is removed; if provided, only the specific virtual artifact instance is removed. |
|
|
667
283
|
|
|
668
|
-
###
|
|
284
|
+
### ArtifactObserver
|
|
669
285
|
|
|
670
|
-
|
|
286
|
+
| Method | Description |
|
|
287
|
+
| :---------------------------- | :---------------------------------------------------------------------------------------------------------- |
|
|
288
|
+
| `get()` | Returns the current cached snapshot of the artifact. |
|
|
289
|
+
| `subscribe(callback, eager?)` | Subscribes to changes. The callback is invoked whenever the artifact state changes (Ready, Error, Pending). |
|
|
290
|
+
| `resolve()` | Manually triggers the resolution of the artifact if it is currently idle. |
|
|
671
291
|
|
|
672
|
-
###
|
|
292
|
+
### ArtifactFactoryContext
|
|
673
293
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
294
|
+
| Method | Description |
|
|
295
|
+
| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
|
|
296
|
+
| `use(callback)` | The primary method for declaring dependencies. Any `resolve` or `select` calls inside the callback are registered as dependencies. |
|
|
297
|
+
| `onCleanup(cleanup)` | Registers a function to be executed before the artifact is rebuilt or disposed. |
|
|
298
|
+
| `onDispose(callback)` | Registers a function to be executed be executed before the artifact is permanently removed. |
|
|
299
|
+
| `stream(callback)` | Initializes a streaming process for a Singleton artifact. |
|
|
300
|
+
| `state()` | Returns a non-reactive read of the current global state. |
|
|
679
301
|
|
|
680
|
-
|
|
302
|
+
### ArtifactTemplate
|
|
681
303
|
|
|
682
|
-
|
|
304
|
+
| Method | Description |
|
|
305
|
+
| :--------- | :---------------------------------------------------------------------- |
|
|
306
|
+
| `key` | Unique identifier for the artifact. |
|
|
307
|
+
| `factory` | The factory function that creates the artifact instance. |
|
|
308
|
+
| `scope` | `singleton` (default) or `transient`. |
|
|
309
|
+
| `lazy` | If `true` (default), the artifact is built only when requested. |
|
|
310
|
+
| `timeout` | Maximum time allowed for the factory to complete. |
|
|
311
|
+
| `retries` | Number of times to retry the factory on failure. |
|
|
312
|
+
| `debounce` | Base debounce time in milliseconds for invalidation events to collapse. |
|
|
313
|
+
| `paramKey` | Function to generate a unique key for parameterized artifacts. |
|
|
683
314
|
|
|
684
|
-
###
|
|
315
|
+
### ResolvedArtifact
|
|
685
316
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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.
|
|
317
|
+
| Property | Description |
|
|
318
|
+
| :--------- | :----------------------------------------------------------------------------------- |
|
|
319
|
+
| `instance` | The resolved artifact instance. Available only when `ready` is true. |
|
|
320
|
+
| `ready` | Boolean indicating if the artifact is resolved and available for use. |
|
|
321
|
+
| `error` | The error caught during the factory execution. Available only when `ready` is false. |
|
|
694
322
|
|
|
695
|
-
|
|
323
|
+
#### Error Handling & Recovery
|
|
696
324
|
|
|
697
|
-
|
|
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.
|
|
325
|
+
Since `ResolvedArtifact` is a discriminated union, you can safely narrow the type of the instance or the error by checking the `ready` flag:
|
|
706
326
|
|
|
707
|
-
|
|
327
|
+
```typescript
|
|
328
|
+
const artifact = await container.resolve("userProfile");
|
|
708
329
|
|
|
709
|
-
|
|
330
|
+
if (artifact.ready) {
|
|
331
|
+
// Type is narrowed to ReadyArtifact: instance is available
|
|
332
|
+
console.log(artifact.instance);
|
|
333
|
+
} else {
|
|
334
|
+
// Type is narrowed to ErrorArtifact: error is available
|
|
335
|
+
console.error("Failed to resolve artifact:", artifact.error);
|
|
710
336
|
|
|
711
|
-
|
|
337
|
+
// Optionally trigger a manual rebuild to recover from transient failures
|
|
338
|
+
// await container.invalidate("userProfile");
|
|
339
|
+
}
|
|
340
|
+
```
|
|
712
341
|
|
|
713
|
-
###
|
|
342
|
+
### ArtifactStreamContext
|
|
714
343
|
|
|
715
|
-
|
|
344
|
+
| Method | Description |
|
|
345
|
+
| { "type": "text", "text": "| `emit(value)` | Emits a new value, triggering invalidation of dependents. |
|
|
346
|
+
| `set(update)` | Dispatches a state update to the global store. |
|
|
347
|
+
| `signal` | AbortSignal to stop the stream. |
|