@asaidimu/utils-artifacts 4.1.1 → 5.0.1
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 +527 -604
- package/index.d.mts +2 -4
- package/index.d.ts +2 -4
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,52 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @asaidimu/utils-artifacts
|
|
2
2
|
|
|
3
|
-
A powerful
|
|
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
5
|
[](https://www.npmjs.com/package/@asaidimu/utils-artifacts)
|
|
6
|
-
[](https://
|
|
7
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/asaidimu/erp-utils/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
## 🚀 Quick Links
|
|
12
|
+
|
|
13
|
+
- [Overview & Features](#-overview--features)
|
|
14
|
+
- [Installation & Setup](#-installation--setup)
|
|
15
|
+
- [Usage Documentation](#-usage-documentation)
|
|
16
|
+
- [Basic Usage](#basic-usage)
|
|
17
|
+
- [Registering Artifacts](#registering-artifacts)
|
|
18
|
+
- [Resolving & Requiring Artifacts](#resolving--requiring-artifacts)
|
|
19
|
+
- [Watching Artifact Changes](#watching-artifact-changes)
|
|
20
|
+
- [Reactive Dependencies (State Selection)](#reactive-dependencies-state-selection)
|
|
21
|
+
- [Artifact Lifecycle (Cleanup & Dispose)](#artifact-lifecycle-cleanup--dispose)
|
|
22
|
+
- [Streaming Artifacts](#streaming-artifacts)
|
|
23
|
+
- [Invalidating Artifacts](#invalidating-artifacts)
|
|
24
|
+
- [Debugging Artifacts](#debugging-artifacts)
|
|
25
|
+
- [Retry Utility](#retry-utility)
|
|
26
|
+
- [Concurrency Utilities (Once & Serializer)](#concurrency-utilities-once--serializer)
|
|
27
|
+
- [Project Architecture](#-project-architecture)
|
|
28
|
+
- [Development & Contributing](#-development--contributing)
|
|
29
|
+
- [Additional Information](#-additional-information)
|
|
26
30
|
|
|
27
31
|
---
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
This package is currently in **beta**. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!
|
|
33
|
+
## ✨ Overview & Features
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
`@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.
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
### Key Features
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* **
|
|
41
|
-
* **
|
|
42
|
-
* **
|
|
43
|
-
* **
|
|
44
|
-
* **
|
|
45
|
-
* **
|
|
46
|
-
* **Asynchronous Artifact Resolution**: Supports `async`/`await` in artifact factories, with configurable `retries` for external failures and `timeout` settings for long-running operations.
|
|
47
|
-
* **`ArtifactObserver` for Reactive Observation**: Use `container.watch(key)` to get an observer that allows subscribing to an artifact's resolved value over time, providing reactive updates to UI components or other systems.
|
|
48
|
-
* **Live Streaming with `ctx.stream()`**: `Singleton` artifacts can emit multiple values over their lifetime from within their factory using `ctx.stream()`, immediately notifying dependents and watchers of new data. This is ideal for continuous data sources like WebSockets or background processes.
|
|
49
|
-
* **Comprehensive Debugging Info**: `container.debugInfo()` provides a runtime snapshot of the container's state, including artifact statuses, dependencies (artifact-to-artifact and state-to-artifact), and dependents.
|
|
39
|
+
* **Dependency Injection (DI)**: Declare dependencies within artifact factories using `ctx.use`, `ctx.resolve`, and `ctx.require`.
|
|
40
|
+
* **Reactive State Management**: Automatically re-evaluate artifacts when slices of a global `DataStore` (or any compatible state management system) change via `ctx.select`.
|
|
41
|
+
* **Flexible Scoping**: Supports `Singleton` (single, cached instance) and `Transient` (new instance per resolution) artifact scopes.
|
|
42
|
+
* **Advanced Lifecycle Management**: `onCleanup` for instance-specific cleanup and `onDispose` for permanent resource release.
|
|
43
|
+
* **Streaming Artifacts**: Use `ctx.stream` to build long-lived artifacts that continuously emit new values, ideal for real-time data, web sockets, or periodic tasks.
|
|
44
|
+
* **Robust Error Handling & Retries**: Integrated retry logic (`Retry` utility) for resilient operations and a comprehensive `ArtifactError` hierarchy for clear diagnostics.
|
|
45
|
+
* **Concurrency Primitives**: Internal `Once` and `Serializer` utilities for managing race conditions and sequential execution of async operations.
|
|
46
|
+
* **Debuggability**: `container.debugInfo()` provides a snapshot of the artifact graph, statuses, and dependencies for easy troubleshooting.
|
|
47
|
+
* **Watcher API**: `container.watch()` allows external consumers to subscribe to artifact value changes without directly resolving them.
|
|
48
|
+
* **Circular Dependency Detection**: Prevents infinite loops during resolution by detecting and reporting cycles in the dependency graph.
|
|
50
49
|
|
|
51
50
|
---
|
|
52
51
|
|
|
@@ -54,761 +53,688 @@ This package is ideal for building complex, event-driven systems, front-end appl
|
|
|
54
53
|
|
|
55
54
|
### Prerequisites
|
|
56
55
|
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* Node.js (LTS recommended)
|
|
57
|
+
* npm or Yarn (package manager)
|
|
58
|
+
* A reactive data store library that adheres to the `DataStore` interface (e.g., `@asaidimu/utils-store`).
|
|
60
59
|
|
|
61
60
|
### Installation Steps
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
To install the library, use your preferred package manager:
|
|
64
63
|
|
|
65
64
|
```bash
|
|
66
|
-
# With Bun
|
|
67
|
-
bun add @asaidimu/utils-artifacts
|
|
68
|
-
|
|
69
|
-
# With npm
|
|
70
65
|
npm install @asaidimu/utils-artifacts
|
|
71
|
-
|
|
72
|
-
# With Yarn
|
|
66
|
+
# or
|
|
73
67
|
yarn add @asaidimu/utils-artifacts
|
|
74
68
|
```
|
|
75
69
|
|
|
76
70
|
### Configuration
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
`@asaidimu/utils-artifacts` is a library and does not require global configuration files. Its primary setup involves initializing the `ArtifactContainer` with an instance of a `DataStore`.
|
|
79
73
|
|
|
80
|
-
|
|
74
|
+
### Verification
|
|
75
|
+
|
|
76
|
+
You can verify the installation by attempting to import and register a basic artifact:
|
|
81
77
|
|
|
82
78
|
```typescript
|
|
83
79
|
import { ArtifactContainer } from '@asaidimu/utils-artifacts';
|
|
84
|
-
import { ReactiveDataStore } from '@asaidimu/utils-store'; //
|
|
80
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming you have this store
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
data: number[];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const initialAppState: AppState = {
|
|
93
|
-
config: { theme: 'dark', debugMode: true },
|
|
94
|
-
user: { id: 'user-123', name: 'Augustine' },
|
|
95
|
-
data: [1, 2, 3]
|
|
96
|
-
};
|
|
82
|
+
// Define your application's global state and artifact registry types
|
|
83
|
+
type AppState = { count: number; };
|
|
84
|
+
type AppRegistry = { myService: string; };
|
|
97
85
|
|
|
98
|
-
const store = new ReactiveDataStore<AppState>(
|
|
99
|
-
|
|
100
|
-
// Initialize the ArtifactContainer
|
|
101
|
-
// The generic arguments <TRegistry, TState> are inferred or can be explicitly provided.
|
|
102
|
-
const container = new ArtifactContainer<Record<string, any>, AppState>(store);
|
|
103
|
-
|
|
104
|
-
// Now you can start registering artifacts
|
|
105
|
-
```
|
|
86
|
+
const store = new ReactiveDataStore<AppState>({ count: 0 });
|
|
87
|
+
const container = new ArtifactContainer<AppRegistry, AppState>(store);
|
|
106
88
|
|
|
107
|
-
|
|
89
|
+
container.register({
|
|
90
|
+
key: 'myService',
|
|
91
|
+
factory: async ({ use }) => {
|
|
92
|
+
const currentCount = await use(({ select }) => select(s => s.count));
|
|
93
|
+
return `Service is running with count: ${currentCount}`;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
108
96
|
|
|
109
|
-
|
|
97
|
+
async function runExample() {
|
|
98
|
+
const service = await container.resolve('myService');
|
|
99
|
+
console.log(service.instance); // Expected: Service is running with count: 0
|
|
100
|
+
}
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
import { ArtifactContainer } from '@asaidimu/utils-artifacts';
|
|
113
|
-
// Assuming a mock or real DataStore is available
|
|
114
|
-
const mockStore = {
|
|
115
|
-
get: () => ({}), // Returns an empty object for state
|
|
116
|
-
watch: () => () => {}, // Returns a no-op unsubscribe function
|
|
117
|
-
set: async () => ({}), // Returns a no-op set function
|
|
118
|
-
};
|
|
119
|
-
const container = new ArtifactContainer(mockStore);
|
|
120
|
-
console.log('ArtifactContainer initialized successfully!');
|
|
121
|
-
// If no errors, the installation is successful.
|
|
102
|
+
runExample();
|
|
122
103
|
```
|
|
123
104
|
|
|
124
105
|
---
|
|
125
106
|
|
|
126
|
-
##
|
|
107
|
+
## 📖 Usage Documentation
|
|
127
108
|
|
|
128
|
-
### Basic
|
|
109
|
+
### Basic Usage
|
|
129
110
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#### Singleton Scope
|
|
133
|
-
|
|
134
|
-
A `Singleton` artifact is created only once. Subsequent `resolve` calls or reactive updates will return or use the same instance, unless its dependencies change, which triggers a rebuild. This is the default scope.
|
|
111
|
+
The core of the library is the `ArtifactContainer`. You initialize it with a `DataStore` that manages your application's global state.
|
|
135
112
|
|
|
136
113
|
```typescript
|
|
137
114
|
import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
|
|
138
115
|
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
139
116
|
|
|
140
|
-
|
|
141
|
-
|
|
117
|
+
// 1. Define your application's global state and artifact registry types
|
|
118
|
+
interface AppState {
|
|
119
|
+
appName: string;
|
|
120
|
+
version: string;
|
|
121
|
+
config: {
|
|
122
|
+
theme: 'light' | 'dark';
|
|
123
|
+
apiUrl: string;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface AppRegistry {
|
|
128
|
+
logger: LoggerService;
|
|
129
|
+
apiClient: ApiClient;
|
|
130
|
+
themeService: 'light' | 'dark';
|
|
131
|
+
appInfo: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Example DataStore (from @asaidimu/utils-store)
|
|
135
|
+
const appStore = new ReactiveDataStore<AppState>({
|
|
136
|
+
appName: 'My App',
|
|
137
|
+
version: '1.0.0',
|
|
138
|
+
config: {
|
|
139
|
+
theme: 'light',
|
|
140
|
+
apiUrl: 'https://api.example.com',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
// 2. Instantiate the ArtifactContainer
|
|
145
|
+
const container = new ArtifactContainer<AppRegistry, AppState>(appStore);
|
|
146
|
+
|
|
147
|
+
// 3. Define and register your artifact factories
|
|
148
|
+
class LoggerService {
|
|
149
|
+
constructor(private prefix: string) {}
|
|
150
|
+
log(message: string) {
|
|
151
|
+
console.log(`[${this.prefix}] ${message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
container.register({
|
|
156
|
+
key: 'logger',
|
|
157
|
+
scope: ArtifactScopes.Singleton, // Ensure only one instance of the logger
|
|
158
|
+
factory: () => new LoggerService('APP'),
|
|
159
|
+
});
|
|
144
160
|
|
|
145
161
|
container.register({
|
|
146
|
-
key: '
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
key: 'apiClient',
|
|
163
|
+
scope: ArtifactScopes.Singleton,
|
|
164
|
+
factory: async ({ use }) => {
|
|
165
|
+
// ApiClient depends on the logger and state.config.apiUrl
|
|
166
|
+
const logger = await use(({ require }) => require('logger'));
|
|
167
|
+
const apiUrl = await use(({ select }) => select(state => state.config.apiUrl));
|
|
168
|
+
|
|
169
|
+
logger.log(`Initializing API client for ${apiUrl}`);
|
|
150
170
|
return {
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
fetchData: async (path: string) => {
|
|
172
|
+
logger.log(`Fetching from ${apiUrl}${path}`);
|
|
173
|
+
// Simulate API call
|
|
174
|
+
await new Promise(res => setTimeout(res, 100));
|
|
175
|
+
return { message: `Data from ${apiUrl}${path}` };
|
|
176
|
+
}
|
|
153
177
|
};
|
|
154
178
|
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
container.register({
|
|
182
|
+
key: 'themeService',
|
|
155
183
|
scope: ArtifactScopes.Singleton,
|
|
156
|
-
|
|
184
|
+
factory: async ({ use }) => {
|
|
185
|
+
// Theme service depends directly on state
|
|
186
|
+
return await use(({ select }) => select(state => state.config.theme));
|
|
187
|
+
},
|
|
157
188
|
});
|
|
158
189
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
container.register({
|
|
191
|
+
key: 'appInfo',
|
|
192
|
+
scope: ArtifactScopes.Singleton,
|
|
193
|
+
factory: async ({ use }) => {
|
|
194
|
+
// App info depends on other artifacts and state
|
|
195
|
+
const logger = await use(({ require }) => require('logger'));
|
|
196
|
+
const apiClient = await use(({ require }) => require('apiClient'));
|
|
197
|
+
const appName = await use(({ select }) => select(s => s.appName));
|
|
198
|
+
|
|
199
|
+
const data = await apiClient.fetchData('/info');
|
|
200
|
+
logger.log(`App Info: ${appName}, Data: ${data.message}`);
|
|
201
|
+
return `App: ${appName}, API Data: ${data.message}`;
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// 4. Resolve artifacts to use them
|
|
206
|
+
async function main() {
|
|
207
|
+
const logger = await container.resolve('logger');
|
|
208
|
+
logger.instance?.log('Application started.');
|
|
162
209
|
|
|
163
|
-
|
|
164
|
-
console.log(
|
|
210
|
+
const appInfo = await container.resolve('appInfo');
|
|
211
|
+
console.log(appInfo.instance); // Logs the app information after API call
|
|
165
212
|
|
|
166
|
-
|
|
167
|
-
console.
|
|
168
|
-
|
|
213
|
+
// Example of reacting to state changes
|
|
214
|
+
console.log('\n--- Changing theme ---');
|
|
215
|
+
await appStore.set(s => ({ ...s, config: { ...s.config, theme: 'dark' } }));
|
|
169
216
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
217
|
+
// Because themeService depends on state.config.theme, it will be rebuilt
|
|
218
|
+
// Any artifact that depends on themeService would also be rebuilt.
|
|
219
|
+
const newTheme = await container.resolve('themeService');
|
|
220
|
+
console.log('New Theme:', newTheme.instance); // Expected: dark
|
|
174
221
|
}
|
|
175
222
|
|
|
176
|
-
|
|
223
|
+
main();
|
|
177
224
|
```
|
|
178
225
|
|
|
179
|
-
|
|
226
|
+
### Registering Artifacts
|
|
180
227
|
|
|
181
|
-
|
|
228
|
+
Use `container.register()` to define an artifact. Each artifact requires a unique `key` and a `factory` function.
|
|
182
229
|
|
|
183
230
|
```typescript
|
|
184
|
-
import {
|
|
185
|
-
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
186
|
-
|
|
187
|
-
const store = new ReactiveDataStore({});
|
|
188
|
-
const container = new ArtifactContainer(store);
|
|
231
|
+
import { ArtifactScopes } from '@asaidimu/utils-artifacts';
|
|
189
232
|
|
|
190
233
|
container.register({
|
|
191
|
-
key: '
|
|
192
|
-
factory: () =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
scope: ArtifactScopes.Transient,
|
|
234
|
+
key: 'myArtifact',
|
|
235
|
+
factory: () => 'Hello, Artifact!',
|
|
236
|
+
// Optional parameters:
|
|
237
|
+
scope: ArtifactScopes.Singleton, // 'singleton' (default) or 'transient'
|
|
238
|
+
lazy: true, // true (default) for singletons, false to build on registration
|
|
239
|
+
timeout: 5000, // Max time in ms for factory to complete
|
|
240
|
+
retries: 3, // Number of retries on factory failure
|
|
241
|
+
debounce: 100, // Delay in ms for rebuilding on dependency changes
|
|
200
242
|
});
|
|
243
|
+
```
|
|
201
244
|
|
|
202
|
-
|
|
203
|
-
const instance1 = await container.resolve('myTransientFactory');
|
|
204
|
-
const instance2 = await container.resolve('myTransientFactory');
|
|
205
|
-
|
|
206
|
-
console.log('Instance 1:', instance1.instance);
|
|
207
|
-
console.log('Instance 2:', instance2.instance);
|
|
208
|
-
|
|
209
|
-
console.assert(instance1.instance !== instance2.instance, 'Transient should return different instances');
|
|
210
|
-
console.assert(instance1.ready && instance2.ready, 'Transient instances should be ready immediately');
|
|
245
|
+
The `factory` function receives an `ArtifactFactoryContext` object:
|
|
211
246
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
247
|
+
```typescript
|
|
248
|
+
interface ArtifactFactoryContext<TRegistry, TState, TArtifact> {
|
|
249
|
+
state(): TState; // Get current global state (non-reactive)
|
|
250
|
+
previous?: TArtifact; // Previous instance (for singletons on rebuild)
|
|
251
|
+
use<R>(callback: (ctx: UseDependencyContext<TRegistry, TState>) => R | Promise<R>): Promise<R>;
|
|
252
|
+
onCleanup(cleanup: ArtifactCleanup): void; // Register cleanup for current instance
|
|
253
|
+
onDispose(callback: ArtifactCleanup): void; // Register cleanup for artifact (permanent)
|
|
254
|
+
stream(callback: (ctx: ArtifactStreamContext<TState, TArtifact>) => ...): void; // Start streaming values (singletons only)
|
|
217
255
|
}
|
|
218
256
|
|
|
219
|
-
|
|
257
|
+
interface UseDependencyContext<TRegistry, TState> {
|
|
258
|
+
resolve<K extends keyof TRegistry>(key: K): Promise<ResolvedArtifact<TRegistry[K]>>; // Resolve an artifact (returns ResolvedArtifact)
|
|
259
|
+
require<K extends keyof TRegistry>(key: K): Promise<TRegistry[K]>; // Resolve an artifact (throws on error, returns instance directly)
|
|
260
|
+
select<S>(selector: (state: TState) => S): S; // Select state slice (reactive)
|
|
261
|
+
}
|
|
220
262
|
```
|
|
221
263
|
|
|
222
|
-
###
|
|
264
|
+
### Resolving & Requiring Artifacts
|
|
223
265
|
|
|
224
|
-
|
|
266
|
+
* `container.resolve(key)`: Returns a `Promise<ResolvedArtifact<T>>`. `ResolvedArtifact` is a union type that can be `ReadyArtifact`, `ErrorArtifact`, or `PendingArtifact`. You should check the `ready` and `error` properties.
|
|
267
|
+
* `container.require(key)`: Returns a `Promise<T>` directly. If resolution fails or the artifact has an error, it will throw the error. Use this when you are certain the artifact will resolve successfully.
|
|
225
268
|
|
|
226
269
|
```typescript
|
|
227
|
-
import {
|
|
228
|
-
|
|
270
|
+
import { ArtifactError } from '@asaidimu/utils-artifacts';
|
|
271
|
+
|
|
272
|
+
// Using resolve (recommended for robust error handling)
|
|
273
|
+
const myArtifactResult = await container.resolve('myArtifact');
|
|
274
|
+
if (myArtifactResult.ready) {
|
|
275
|
+
console.log('Artifact instance:', myArtifactResult.instance);
|
|
276
|
+
} else if (myArtifactResult.error) {
|
|
277
|
+
console.error('Artifact failed to resolve:', myArtifactResult.error);
|
|
278
|
+
} else {
|
|
279
|
+
console.log('Artifact is pending/idle.');
|
|
280
|
+
}
|
|
229
281
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
282
|
+
// Using require (for simpler usage when errors are handled upstream or unexpected)
|
|
283
|
+
try {
|
|
284
|
+
const myArtifactInstance = await container.require('myArtifact');
|
|
285
|
+
console.log('Artifact instance:', myArtifactInstance);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof ArtifactError) {
|
|
288
|
+
console.error('Artifact system error:', error.message);
|
|
289
|
+
} else {
|
|
290
|
+
console.error('Artifact runtime error:', error);
|
|
291
|
+
}
|
|
233
292
|
}
|
|
293
|
+
```
|
|
234
294
|
|
|
235
|
-
|
|
236
|
-
theme: 'dark',
|
|
237
|
-
notificationsEnabled: true,
|
|
238
|
-
});
|
|
239
|
-
const container = new ArtifactContainer<any, AppState>(store);
|
|
295
|
+
### Watching Artifact Changes
|
|
240
296
|
|
|
241
|
-
|
|
242
|
-
container.register({
|
|
243
|
-
key: 'uiConfiguration',
|
|
244
|
-
factory: async ({ use }) => {
|
|
245
|
-
uiComponentBuilds++;
|
|
246
|
-
const theme = await use((ctx) => ctx.select((state) => state.theme));
|
|
247
|
-
const notifications = await use((ctx) => ctx.select((state) => state.notificationsEnabled));
|
|
297
|
+
The `watch()` method provides an observer pattern to react to artifact changes without needing to repeatedly call `resolve()`. It's particularly useful for UI frameworks.
|
|
248
298
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
},
|
|
252
|
-
scope: ArtifactScopes.Singleton,
|
|
253
|
-
});
|
|
299
|
+
```typescript
|
|
300
|
+
const myServiceWatcher = container.watch('myService');
|
|
254
301
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
uiConfig = await container.resolve('uiConfiguration');
|
|
267
|
-
console.assert(uiConfig.instance?.theme === 'light', 'Updated theme should be light');
|
|
268
|
-
console.assert(uiComponentBuilds === 2, 'Build count should increment after state change');
|
|
269
|
-
|
|
270
|
-
// Simulate another state change (different property)
|
|
271
|
-
console.log('\n--- Disabling notifications ---');
|
|
272
|
-
await store.set({ notificationsEnabled: false });
|
|
273
|
-
|
|
274
|
-
// Resolve again
|
|
275
|
-
uiConfig = await container.resolve('uiConfiguration');
|
|
276
|
-
console.assert(uiConfig.instance?.notifications === false, 'Notifications should be false');
|
|
277
|
-
console.assert(uiComponentBuilds === 3, 'Build count should increment after another state change');
|
|
278
|
-
|
|
279
|
-
// Output:
|
|
280
|
-
// UI Config Build 1: Theme=dark, Notifications=true
|
|
281
|
-
//
|
|
282
|
-
// --- Changing theme to light ---
|
|
283
|
-
// UI Config Build 2: Theme=light, Notifications=true
|
|
284
|
-
//
|
|
285
|
-
// --- Disabling notifications ---
|
|
286
|
-
// UI Config Build 3: Theme=light, Notifications=false
|
|
287
|
-
}
|
|
302
|
+
const unsubscribe = myServiceWatcher.subscribe((resolvedArtifact) => {
|
|
303
|
+
if (resolvedArtifact.ready) {
|
|
304
|
+
console.log('myService updated:', resolvedArtifact.instance);
|
|
305
|
+
} else if (resolvedArtifact.error) {
|
|
306
|
+
console.error('myService error:', resolvedArtifact.error);
|
|
307
|
+
}
|
|
308
|
+
// The `get()` method can also be used inside the callback or outside
|
|
309
|
+
// to get the current state of the artifact.
|
|
310
|
+
const current = myServiceWatcher.get();
|
|
311
|
+
console.log('Current state from get():', current.instance);
|
|
312
|
+
});
|
|
288
313
|
|
|
289
|
-
|
|
314
|
+
// To stop receiving updates:
|
|
315
|
+
unsubscribe();
|
|
290
316
|
```
|
|
291
317
|
|
|
292
|
-
###
|
|
318
|
+
### Reactive Dependencies (State Selection)
|
|
293
319
|
|
|
294
|
-
Artifacts can
|
|
320
|
+
Artifacts can react to changes in the global `DataStore` by using `ctx.select()`.
|
|
295
321
|
|
|
296
322
|
```typescript
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const store = new ReactiveDataStore({});
|
|
301
|
-
const container = new ArtifactContainer(store);
|
|
323
|
+
interface UserSettings { userId: string; theme: string; };
|
|
324
|
+
type AppRegistry = { userPreference: string };
|
|
302
325
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
key: 'dbConnection',
|
|
306
|
-
factory: async () => {
|
|
307
|
-
dbConnectionBuilds++;
|
|
308
|
-
console.log(`Establishing DB Connection (${dbConnectionBuilds})...`);
|
|
309
|
-
await new Promise(res => setTimeout(res, 50)); // Simulate async work
|
|
310
|
-
return { client: 'PostgreSQL', status: 'connected' };
|
|
311
|
-
},
|
|
312
|
-
scope: ArtifactScopes.Singleton,
|
|
313
|
-
});
|
|
326
|
+
const userStore = new ReactiveDataStore<UserSettings>({ userId: 'guest', theme: 'light' });
|
|
327
|
+
const userContainer = new ArtifactContainer<AppRegistry, UserSettings>(userStore);
|
|
314
328
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
key: 'userRepository',
|
|
329
|
+
userContainer.register({
|
|
330
|
+
key: 'userPreference',
|
|
318
331
|
factory: async ({ use }) => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
dbClient: db.client,
|
|
324
|
-
findUser: (id: string) => `User ${id} from ${db.client}`
|
|
325
|
-
};
|
|
332
|
+
// This artifact will be rebuilt if state.theme changes
|
|
333
|
+
const theme = await use(({ select }) => select(state => state.theme));
|
|
334
|
+
return `Current theme is: ${theme}`;
|
|
326
335
|
},
|
|
327
|
-
scope: ArtifactScopes.Singleton,
|
|
328
336
|
});
|
|
329
337
|
|
|
330
|
-
async function
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
await dbConnection.invalidate(true); // `true` forces immediate rebuild
|
|
341
|
-
|
|
342
|
-
// Resolve UserRepository again - it should have been rebuilt
|
|
343
|
-
userRepo = await container.resolve('userRepository');
|
|
344
|
-
console.assert(dbConnectionBuilds === 2, 'DB Connection rebuilt');
|
|
345
|
-
console.assert(userRepositoryBuilds === 2, 'User Repository rebuilt due to DB change');
|
|
346
|
-
console.log('User Repo after invalidation:', userRepo.instance?.findUser('2'));
|
|
347
|
-
|
|
348
|
-
// Output:
|
|
349
|
-
// Establishing DB Connection (1)...
|
|
350
|
-
// Building UserRepository (1)...
|
|
351
|
-
// User Repo initialized: User 1 from PostgreSQL
|
|
352
|
-
//
|
|
353
|
-
// --- Invalidating DB Connection ---
|
|
354
|
-
// Establishing DB Connection (2)...
|
|
355
|
-
// Building UserRepository (2)...
|
|
356
|
-
// User Repo after invalidation: User 2 from PostgreSQL
|
|
338
|
+
async function runReactiveExample() {
|
|
339
|
+
let preference = await userContainer.resolve('userPreference');
|
|
340
|
+
console.log(preference.instance); // Output: Current theme is: light
|
|
341
|
+
|
|
342
|
+
// Update the store, which will trigger 'userPreference' to rebuild
|
|
343
|
+
await userStore.set({ theme: 'dark' });
|
|
344
|
+
|
|
345
|
+
// Resolve again to get the new instance
|
|
346
|
+
preference = await userContainer.resolve('userPreference');
|
|
347
|
+
console.log(preference.instance); // Output: Current theme is: dark
|
|
357
348
|
}
|
|
358
349
|
|
|
359
|
-
|
|
350
|
+
runReactiveExample();
|
|
360
351
|
```
|
|
361
352
|
|
|
362
|
-
### Lifecycle
|
|
363
|
-
|
|
364
|
-
Artifacts can register cleanup functions to manage resources, ensuring proper release when instances are no longer needed.
|
|
353
|
+
### Artifact Lifecycle (Cleanup & Dispose)
|
|
365
354
|
|
|
366
|
-
* `onCleanup(
|
|
367
|
-
* `onDispose(
|
|
355
|
+
* `ctx.onCleanup(fn)`: Registers a function to run *before* a singleton artifact is rebuilt (due to invalidation) or before a transient artifact instance is discarded. Use this for instance-specific resource release (e.g., clearing timers, event listeners).
|
|
356
|
+
* `ctx.onDispose(fn)`: Registers a function to run *only when the artifact is permanently unregistered* from the container or the container itself is disposed. Use this for permanent resource release (e.g., closing database connections, unsubscribing from global events).
|
|
368
357
|
|
|
369
358
|
```typescript
|
|
370
|
-
import { ArtifactContainer, ArtifactScopes } from '@asaidimu/utils-artifacts';
|
|
371
|
-
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
372
|
-
|
|
373
|
-
const store = new ReactiveDataStore({});
|
|
374
|
-
const container = new ArtifactContainer(store);
|
|
375
|
-
|
|
376
|
-
let currentInterval: any;
|
|
377
|
-
|
|
378
359
|
container.register({
|
|
379
|
-
key: '
|
|
360
|
+
key: 'myResource',
|
|
361
|
+
scope: ArtifactScopes.Singleton,
|
|
380
362
|
factory: ({ onCleanup, onDispose }) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
currentInterval = setInterval(() => {
|
|
384
|
-
count++;
|
|
385
|
-
// console.log(`Tick: ${count}`); // Uncomment to see live ticks
|
|
386
|
-
}, 1000);
|
|
363
|
+
const resource = { id: Math.random(), intervalId: setInterval(() => {}, 1000) };
|
|
364
|
+
console.log(`Resource ${resource.id} created.`);
|
|
387
365
|
|
|
388
366
|
onCleanup(() => {
|
|
389
|
-
console.log(
|
|
390
|
-
clearInterval(
|
|
367
|
+
console.log(`Cleaning up instance ${resource.id}...`);
|
|
368
|
+
clearInterval(resource.intervalId);
|
|
391
369
|
});
|
|
392
370
|
|
|
393
371
|
onDispose(() => {
|
|
394
|
-
console.log(
|
|
395
|
-
// Additional
|
|
372
|
+
console.log(`Disposing artifact 'myResource'.`);
|
|
373
|
+
// Additional permanent resource release here
|
|
396
374
|
});
|
|
397
375
|
|
|
398
|
-
return
|
|
376
|
+
return resource;
|
|
399
377
|
},
|
|
400
|
-
scope: ArtifactScopes.Singleton,
|
|
401
|
-
lazy: false, // Eagerly build
|
|
402
378
|
});
|
|
403
379
|
|
|
404
|
-
async function
|
|
405
|
-
await container.resolve('
|
|
406
|
-
|
|
407
|
-
await
|
|
408
|
-
|
|
409
|
-
console.log('\n--- Invalidating Ticker (will trigger onCleanup and rebuild) ---');
|
|
410
|
-
const ticker = await container.resolve('ticker'); // Get current to invalidate
|
|
411
|
-
await ticker.invalidate(true); // Force rebuild
|
|
412
|
-
console.log('Ticker rebuilt. Running for 2 more seconds...');
|
|
413
|
-
await new Promise(res => setTimeout(res, 2000));
|
|
414
|
-
|
|
415
|
-
console.log('\n--- Unregistering Ticker (will trigger onDispose) ---');
|
|
416
|
-
await container.unregister('ticker');
|
|
417
|
-
console.log('Ticker unregistered.');
|
|
380
|
+
async function lifecycleExample() {
|
|
381
|
+
await container.resolve('myResource');
|
|
382
|
+
// Simulate an invalidation (e.g., a dependency changed)
|
|
383
|
+
await container.invalidate('myResource'); // Triggers cleanup, then rebuilds
|
|
384
|
+
await container.resolve('myResource'); // A new instance is now resolved.
|
|
418
385
|
|
|
419
|
-
//
|
|
420
|
-
//
|
|
386
|
+
// When unregistering, onDispose is called
|
|
387
|
+
await container.unregister('myResource'); // Triggers onCleanup (if active), then onDispose
|
|
421
388
|
}
|
|
422
|
-
|
|
423
|
-
runLifecycleHooksExample();
|
|
389
|
+
lifecycleExample();
|
|
424
390
|
```
|
|
425
391
|
|
|
426
|
-
###
|
|
392
|
+
### Streaming Artifacts
|
|
427
393
|
|
|
428
|
-
|
|
394
|
+
Singletons can continuously emit new values using `ctx.stream()`. This is powerful for reactive data sources.
|
|
429
395
|
|
|
430
396
|
```typescript
|
|
431
|
-
|
|
432
|
-
|
|
397
|
+
container.register({
|
|
398
|
+
key: 'counterStream',
|
|
399
|
+
scope: ArtifactScopes.Singleton,
|
|
400
|
+
factory: ({ stream, onCleanup }) => {
|
|
401
|
+
let count = 0;
|
|
402
|
+
let interval: ReturnType<typeof setInterval>;
|
|
433
403
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
404
|
+
stream(async ({ emit, signal }) => {
|
|
405
|
+
console.log('Counter stream started...');
|
|
406
|
+
interval = setInterval(() => {
|
|
407
|
+
if (signal.aborted) {
|
|
408
|
+
console.log('Stream aborted, stopping interval.');
|
|
409
|
+
clearInterval(interval);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
count++;
|
|
413
|
+
emit(count); // Emit the new value
|
|
414
|
+
if (count >= 5) {
|
|
415
|
+
console.log('Count limit reached, stopping stream.');
|
|
416
|
+
clearInterval(interval);
|
|
417
|
+
return; // Stream producer can return to end the stream
|
|
418
|
+
}
|
|
419
|
+
}, 500);
|
|
420
|
+
});
|
|
437
421
|
|
|
438
|
-
|
|
439
|
-
|
|
422
|
+
onCleanup(() => {
|
|
423
|
+
console.log('Cleaning up counter stream instance...');
|
|
424
|
+
// Ensure interval is cleared if stream is aborted/rebuilt
|
|
425
|
+
clearInterval(interval);
|
|
426
|
+
});
|
|
440
427
|
|
|
441
|
-
|
|
442
|
-
container.register({
|
|
443
|
-
key: 'reactiveCounter',
|
|
444
|
-
factory: async ({ use }) => {
|
|
445
|
-
reactiveCounterBuilds++;
|
|
446
|
-
const count = await use((ctx) => ctx.select((state) => state.counter));
|
|
447
|
-
return `Current count: ${count} (Build: ${reactiveCounterBuilds})`;
|
|
428
|
+
return 0; // Initial value before stream starts emitting
|
|
448
429
|
},
|
|
449
|
-
scope: ArtifactScopes.Singleton,
|
|
450
430
|
});
|
|
451
431
|
|
|
452
|
-
async function
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
let unsubscribe = watcher.subscribe((resolved) => {
|
|
457
|
-
if (resolved.ready) {
|
|
458
|
-
console.log('Watcher received update:', resolved.instance);
|
|
459
|
-
} else if (resolved.error) {
|
|
460
|
-
console.error('Watcher received error:', resolved.error.message);
|
|
461
|
-
} else {
|
|
462
|
-
console.log('Watcher received pending update (artifact not yet ready).');
|
|
463
|
-
}
|
|
432
|
+
async function streamExample() {
|
|
433
|
+
const watcher = container.watch('counterStream');
|
|
434
|
+
const unsubscribe = watcher.subscribe((art) => {
|
|
435
|
+
if (art.ready) console.log('Counter value:', art.instance);
|
|
464
436
|
});
|
|
465
437
|
|
|
466
|
-
//
|
|
467
|
-
await new Promise(res => setTimeout(res,
|
|
468
|
-
|
|
469
|
-
console.log('
|
|
470
|
-
await store.set({ counter: 1 });
|
|
471
|
-
await new Promise(res => setTimeout(res, 10));
|
|
472
|
-
|
|
473
|
-
console.log('\n--- Incrementing counter again ---');
|
|
474
|
-
await store.set({ counter: 2 });
|
|
475
|
-
await new Promise(res => setTimeout(res, 10));
|
|
476
|
-
|
|
477
|
-
console.log('\n--- Unsubscribing & Disposing watcher ---');
|
|
478
|
-
unsubscribe(); // Unsubscribe from updates
|
|
479
|
-
|
|
480
|
-
// After all subscribers are gone, the underlying artifact instance for watching transients is cleaned up.
|
|
481
|
-
// The watcher itself can then be disposed, making it unusable.
|
|
482
|
-
await new Promise(res => setTimeout(res, 10)); // Give cleanup a moment
|
|
483
|
-
// For singletons, watcher.dispose() is effectively a no-op as the underlying artifact remains
|
|
484
|
-
// For transients, it would clean up the internal singleton clone.
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
watcher.get(); // Attempt to access after dispose
|
|
488
|
-
} catch (e) {
|
|
489
|
-
if (e instanceof WatcherDisposedError) {
|
|
490
|
-
console.error('Caught expected error after watcher dispose:', e.message);
|
|
491
|
-
} else {
|
|
492
|
-
throw e;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Output:
|
|
497
|
-
// Setting up watcher...
|
|
498
|
-
// Watcher received pending update (artifact not yet ready).
|
|
499
|
-
// Watcher received update: Current count: 0 (Build: 1)
|
|
500
|
-
//
|
|
501
|
-
// --- Incrementing counter ---
|
|
502
|
-
// Watcher received update: Current count: 1 (Build: 2)
|
|
503
|
-
//
|
|
504
|
-
// --- Incrementing counter again ---
|
|
505
|
-
// Watcher received update: Current count: 2 (Build: 3)
|
|
506
|
-
//
|
|
507
|
-
// --- Unsubscribing & Disposing watcher ---
|
|
508
|
-
// Caught expected error after watcher dispose: [ArtifactContainer] Artifact with key:reactiveCounter has already been disposed
|
|
438
|
+
// Keep alive for a few seconds to see stream emissions
|
|
439
|
+
await new Promise(res => setTimeout(res, 3000));
|
|
440
|
+
unsubscribe();
|
|
441
|
+
console.log('Stream watcher unsubscribed.');
|
|
509
442
|
}
|
|
510
|
-
|
|
511
|
-
runWatcherExample();
|
|
443
|
+
streamExample();
|
|
512
444
|
```
|
|
513
445
|
|
|
514
|
-
###
|
|
446
|
+
### Invalidating Artifacts
|
|
515
447
|
|
|
516
|
-
|
|
448
|
+
You can manually trigger an artifact to rebuild, which will also cascade invalidations to its dependents.
|
|
517
449
|
|
|
518
450
|
```typescript
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const store = new ReactiveDataStore({});
|
|
523
|
-
const container = new ArtifactContainer<{ producer: number; consumer: string }, {}>(store);
|
|
451
|
+
// Invalidate a specific artifact
|
|
452
|
+
await container.invalidate('myArtifact');
|
|
524
453
|
|
|
525
|
-
|
|
526
|
-
|
|
454
|
+
// Force immediate rebuild, bypassing any debounce delay
|
|
455
|
+
await container.invalidate('myArtifact', true);
|
|
456
|
+
```
|
|
527
457
|
|
|
528
|
-
|
|
529
|
-
key: "producer",
|
|
530
|
-
factory: ({ stream }) => {
|
|
531
|
-
let value = 0;
|
|
458
|
+
### Debugging Artifacts
|
|
532
459
|
|
|
533
|
-
|
|
534
|
-
stream(async ({ emit, signal }) => {
|
|
535
|
-
console.log("Producer: Stream started.");
|
|
536
|
-
while (!signal.aborted) {
|
|
537
|
-
await new Promise((res) => setTimeout(res, PRODUCER_TIMEOUT)); // Simulate async work
|
|
538
|
-
value++;
|
|
539
|
-
console.log(`Producer: Emitting value: ${value}`);
|
|
540
|
-
await emit(value); // Emit new value, notifying dependents and watchers
|
|
541
|
-
if (value >= PRODUCER_LIMIT) {
|
|
542
|
-
console.log("Producer: Reached limit, stopping stream.");
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
});
|
|
460
|
+
The `debugInfo()` method provides a snapshot of the container's internal state, useful for understanding dependencies, status, and build counts.
|
|
547
461
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
462
|
+
```typescript
|
|
463
|
+
const debugNodes = container.debugInfo();
|
|
464
|
+
debugNodes.forEach(node => {
|
|
465
|
+
console.log(`\nID: ${node.id}`);
|
|
466
|
+
console.log(` Scope: ${node.scope}`);
|
|
467
|
+
console.log(` Status: ${node.status}`); // active, error, idle, building, pending, debouncing
|
|
468
|
+
console.log(` Dependencies (Artifacts): ${node.dependencies.join(', ') || 'None'}`);
|
|
469
|
+
console.log(` Dependencies (State Paths): ${node.stateDependencies.join(', ') || 'None'}`);
|
|
470
|
+
console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
|
|
471
|
+
console.log(` Build Count: ${node.buildCount}`);
|
|
551
472
|
});
|
|
473
|
+
```
|
|
552
474
|
|
|
553
|
-
|
|
554
|
-
key: "consumer",
|
|
555
|
-
factory: async ({ use }) => {
|
|
556
|
-
// This artifact depends on the 'producer'
|
|
557
|
-
const producerValue = await use(({ require }) => require("producer"));
|
|
558
|
-
return `Consumed: ${producerValue}`;
|
|
559
|
-
},
|
|
560
|
-
scope: ArtifactScopes.Singleton,
|
|
561
|
-
});
|
|
475
|
+
### Retry Utility
|
|
562
476
|
|
|
563
|
-
async
|
|
564
|
-
const watcher = container.watch("consumer");
|
|
565
|
-
const receivedValues: string[] = [];
|
|
477
|
+
The `Retry` class provides flexible retry logic for any async operation, supporting various strategies.
|
|
566
478
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
receivedValues.push(resolved.instance);
|
|
570
|
-
console.log(`Watcher: Received "${resolved.instance}"`);
|
|
571
|
-
}
|
|
572
|
-
});
|
|
479
|
+
```typescript
|
|
480
|
+
import { Retry, RetryExhaustedError, RetryPredicates } from '@asaidimu/utils-artifacts/retry';
|
|
573
481
|
|
|
574
|
-
|
|
575
|
-
|
|
482
|
+
const unreliableOperation = async (attempt: number) => {
|
|
483
|
+
if (attempt < 3) {
|
|
484
|
+
console.log(`Unreliable operation failing on attempt ${attempt}`);
|
|
485
|
+
throw new Error('Transient network error');
|
|
486
|
+
}
|
|
487
|
+
console.log(`Unreliable operation succeeding on attempt ${attempt}`);
|
|
488
|
+
return 'Success!';
|
|
489
|
+
};
|
|
576
490
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|
|
580
515
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
516
|
+
// Example with conditional retry based on error type
|
|
517
|
+
const fetchWithRetry = async (url: string) => {
|
|
518
|
+
return Retry.execute(
|
|
519
|
+
async () => {
|
|
520
|
+
const response = await fetch(url);
|
|
521
|
+
if (response.status >= 500) {
|
|
522
|
+
throw { status: response.status, message: 'Server error' }; // Simulate HTTP 5xx
|
|
523
|
+
}
|
|
524
|
+
return response.json();
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
retries: 5,
|
|
528
|
+
strategy: 'conditional',
|
|
529
|
+
shouldRetry: RetryPredicates.any(
|
|
530
|
+
RetryPredicates.networkErrors,
|
|
531
|
+
RetryPredicates.serverErrors,
|
|
532
|
+
RetryPredicates.httpStatus(429) // Also retry on Too Many Requests
|
|
533
|
+
),
|
|
534
|
+
delay: (attempt) => Math.min(100 * Math.pow(2, attempt), 2000), // Custom delay function
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
};
|
|
584
538
|
|
|
585
|
-
|
|
539
|
+
// const data = await fetchWithRetry('https://api.example.com/data');
|
|
586
540
|
}
|
|
587
|
-
|
|
588
|
-
|
|
541
|
+
let retryAttempt = 0; // Used for demonstration purposes only
|
|
542
|
+
runRetryExample();
|
|
589
543
|
```
|
|
590
544
|
|
|
591
|
-
###
|
|
545
|
+
### Concurrency Utilities (Once & Serializer)
|
|
592
546
|
|
|
593
|
-
|
|
547
|
+
`Once` ensures a function runs exactly one time, caching its result. `Serializer` ensures functions run sequentially. These are primarily used internally but are exposed for advanced use cases.
|
|
594
548
|
|
|
595
549
|
```typescript
|
|
596
|
-
import {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
};
|
|
559
|
+
|
|
560
|
+
const [res1, res2, res3] = await Promise.all([
|
|
561
|
+
initialization.do(expensiveInit),
|
|
562
|
+
initialization.do(expensiveInit),
|
|
563
|
+
initialization.do(expensiveInit),
|
|
564
|
+
]);
|
|
565
|
+
|
|
566
|
+
console.log(res1.value, res2.value, res3.value); // All will be 'Initialized Resource'
|
|
567
|
+
// expensiveInit will be called only once.
|
|
601
568
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
async function runDebugInfoExample() {
|
|
631
|
-
await container.resolve('dataProcessor'); // Resolve to build dependencies
|
|
632
|
-
await container.resolve('ephemeralTool'); // Resolve a transient (doesn't cache, but will show up if just resolved)
|
|
633
|
-
|
|
634
|
-
const debugInfo = container.debugInfo();
|
|
635
|
-
console.log('--- Artifact Debug Information ---');
|
|
636
|
-
debugInfo.forEach(node => {
|
|
637
|
-
console.log(`\nID: ${node.id}`);
|
|
638
|
-
console.log(` Scope: ${node.scope}`);
|
|
639
|
-
console.log(` Status: ${node.status}`);
|
|
640
|
-
console.log(` Dependencies (Artifacts): ${node.dependencies.join(', ') || 'None'}`);
|
|
641
|
-
console.log(` Dependencies (State Paths): ${node.stateDependencies.join(', ') || 'None'}`);
|
|
642
|
-
console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
|
|
643
|
-
console.log(` Render Count: ${node.renderCount}`);
|
|
644
|
-
console.log('---');
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
// Example output for 'dataProcessor':
|
|
648
|
-
// ID: dataProcessor
|
|
649
|
-
// Scope: singleton
|
|
650
|
-
// Status: active
|
|
651
|
-
// Dependencies (Artifacts): configService
|
|
652
|
-
// Dependencies (State Paths): None
|
|
653
|
-
// Dependents: None
|
|
654
|
-
// Render Count: 1
|
|
569
|
+
runOnceExample();
|
|
570
|
+
|
|
571
|
+
async function runSerializerExample() {
|
|
572
|
+
const queue = new Serializer<string>();
|
|
573
|
+
const order: string[] = [];
|
|
574
|
+
|
|
575
|
+
const task1 = async () => {
|
|
576
|
+
await new Promise(res => setTimeout(res, 100));
|
|
577
|
+
order.push('Task 1');
|
|
578
|
+
return 'Result 1';
|
|
579
|
+
};
|
|
580
|
+
const task2 = async () => {
|
|
581
|
+
order.push('Task 2');
|
|
582
|
+
return 'Result 2';
|
|
583
|
+
};
|
|
584
|
+
const task3 = async () => {
|
|
585
|
+
await new Promise(res => setTimeout(res, 50));
|
|
586
|
+
order.push('Task 3');
|
|
587
|
+
return 'Result 3';
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
await Promise.all([
|
|
591
|
+
queue.do(task1),
|
|
592
|
+
queue.do(task2),
|
|
593
|
+
queue.do(task3),
|
|
594
|
+
]);
|
|
595
|
+
|
|
596
|
+
console.log(order); // Expected: ['Task 1', 'Task 2', 'Task 3']
|
|
655
597
|
}
|
|
656
|
-
|
|
657
|
-
runDebugInfoExample();
|
|
598
|
+
runSerializerExample();
|
|
658
599
|
```
|
|
659
600
|
|
|
660
601
|
---
|
|
661
602
|
|
|
662
|
-
##
|
|
663
|
-
|
|
664
|
-
The `@asaidimu/utils-artifacts` package is built around the central `ArtifactContainer` class, which orchestrates the lifecycle and dependencies of various "artifacts" (any component or value you register). It is designed to be modular and extensible, promoting maintainability and testability.
|
|
603
|
+
## 🏗️ Project Architecture
|
|
665
604
|
|
|
666
|
-
|
|
605
|
+
The `ArtifactContainer` is designed with a modular architecture, delegating responsibilities to specialized internal components:
|
|
667
606
|
|
|
668
|
-
* **`ArtifactContainer`**: The public
|
|
669
|
-
* **`ArtifactRegistry`**:
|
|
670
|
-
* **`ArtifactCache`**:
|
|
671
|
-
* **`ArtifactDependencyGraph`**: A
|
|
672
|
-
* **`ArtifactManager`**: The core
|
|
673
|
-
* **`ArtifactObserverManager`**:
|
|
674
|
-
* **`
|
|
675
|
-
*
|
|
676
|
-
* **Error Handling (`ArtifactError` hierarchy)**: A dedicated set of custom error classes (`src/artifacts/errors.ts`) provides clear, categorized error messages for system-level issues (e.g., circular dependencies, missing artifacts, illegal operations), simplifying debugging and error handling.
|
|
607
|
+
* **`ArtifactContainer`**: The public API entry point. It orchestrates interactions between the other internal components, providing a unified interface for artifact management.
|
|
608
|
+
* **`ArtifactRegistry`**: Stores the definitions (`ArtifactTemplate`s) of all registered artifacts, mapping unique keys to their factory functions and configuration options.
|
|
609
|
+
* **`ArtifactCache`**: Manages the storage and retrieval of resolved artifact instances, particularly for `Singleton` scoped artifacts. It handles caching, versioning, and state associated with active instances.
|
|
610
|
+
* **`ArtifactDependencyGraph`**: A bidirectional graph (`DependencyGraph`) that maps artifact-to-artifact dependencies and tracks which artifacts depend on state paths. This is crucial for circular dependency detection and efficient invalidation cascades.
|
|
611
|
+
* **`ArtifactManager`**: The core lifecycle manager. It handles the intricate process of building artifacts (executing factories), managing retries and timeouts, orchestrating `onCleanup`/`onDispose`, propagating stream emissions, and managing the reactive invalidation process based on artifact and state dependencies.
|
|
612
|
+
* **`ArtifactObserverManager`**: Manages the `container.watch()` API, maintaining subscriptions from external consumers and notifying them of artifact state changes. It handles reference counting and lazy initialization of watched artifacts.
|
|
613
|
+
* **`Retry`**: A standalone utility providing flexible retry mechanisms (fixed, exponential, jittered, conditional) for any asynchronous operation.
|
|
614
|
+
* **`Once` & `Serializer`**: Low-level concurrency primitives used internally (and exposed) to ensure that asynchronous operations (like artifact builds or stream emissions) execute safely and predictably, avoiding race conditions.
|
|
677
615
|
|
|
678
616
|
### Data Flow
|
|
679
617
|
|
|
680
|
-
1. **Registration
|
|
681
|
-
2. **Resolution (`container.resolve`
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
*
|
|
693
|
-
|
|
694
|
-
* **
|
|
695
|
-
* **
|
|
696
|
-
*
|
|
697
|
-
|
|
698
|
-
*
|
|
699
|
-
* Increment the artifact's internal version.
|
|
700
|
-
* Trigger `invalidate()` on all its dependents and `notifyObservers()`, effectively propagating the streamed value reactively.
|
|
701
|
-
7. **Disposal (`container.dispose` / `container.unregister`)**: All resources associated with an artifact (`onDispose` hooks, state subscriptions, cache entry, graph links) are released. Full container disposal clears everything.
|
|
618
|
+
1. **Registration**: An `ArtifactTemplate` is registered with the `ArtifactRegistry`. A corresponding node is added to the `ArtifactDependencyGraph`.
|
|
619
|
+
2. **Resolution (`container.resolve`)**:
|
|
620
|
+
* The `ArtifactContainer` forwards the request to the `ArtifactManager`.
|
|
621
|
+
* The `Manager` consults the `ArtifactCache`. If a `Singleton` is already built and fresh, it's returned immediately.
|
|
622
|
+
* Otherwise, the `Manager` retrieves the `ArtifactTemplate` from the `Registry`.
|
|
623
|
+
* A factory is executed with an `ArtifactFactoryContext`.
|
|
624
|
+
* Inside the factory:
|
|
625
|
+
* `ctx.use(({ resolve }) => ...)`: Triggers recursive resolution of dependent artifacts. The `Manager` performs cycle detection via the `DependencyGraph` and updates artifact dependencies.
|
|
626
|
+
* `ctx.use(({ select }) => ...)`: Registers state path dependencies with the `Manager`, which then subscribes to these paths via the `DataStore`.
|
|
627
|
+
* `ctx.stream(...)`: For singletons, registers a stream producer that can `emit` new values.
|
|
628
|
+
* `ctx.onCleanup`/`onDispose`: Registers lifecycle hooks.
|
|
629
|
+
* The `Manager` handles retries for factory execution and commits the resulting instance (or error) to the `ArtifactCache`.
|
|
630
|
+
* The `ArtifactCache` packages the result into a `ResolvedArtifact` for the consumer.
|
|
631
|
+
3. **Invalidation**:
|
|
632
|
+
* **State Change**: A change in the `DataStore` (observed by `ArtifactManager` via `store.watch()`) triggers invalidation of dependent artifacts.
|
|
633
|
+
* **Artifact Change**: A dependency being rebuilt or a stream emitting a new value, or manual `container.invalidate()` triggers invalidation.
|
|
634
|
+
* The `ArtifactManager` runs the `onCleanup` hooks for the old instance, removes it from the `ArtifactCache`, and uses the `DependencyGraph` to identify and cascade invalidations to all affected dependents.
|
|
635
|
+
* If configured (e.g., not lazy, has watchers), the `Manager` triggers a rebuild for the artifact.
|
|
636
|
+
* Finally, `ArtifactObserverManager` notifies all active watchers.
|
|
702
637
|
|
|
703
638
|
### Extension Points
|
|
704
639
|
|
|
705
|
-
The primary extension point is the
|
|
706
|
-
* Integrate with any third-party library or framework (e.g., database clients, API wrappers, authentication services).
|
|
707
|
-
* Define complex business logic that reacts to state or other services.
|
|
708
|
-
* Manage external resources like database connections, API clients, or message queues, ensuring they are properly initialized and cleaned up using `onCleanup` and `onDispose`.
|
|
709
|
-
* Provide custom logging, monitoring, or telemetry by injecting those services as dependencies.
|
|
640
|
+
The primary extension point is the `ArtifactFactory` function itself, which receives the `ArtifactFactoryContext`. This context allows artifacts to:
|
|
710
641
|
|
|
711
|
-
|
|
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.
|
|
712
646
|
|
|
713
647
|
---
|
|
714
648
|
|
|
715
|
-
##
|
|
716
|
-
|
|
717
|
-
We welcome contributions! Please read the guidelines below.
|
|
649
|
+
## 🛠️ Development & Contributing
|
|
718
650
|
|
|
719
651
|
### Development Setup
|
|
720
652
|
|
|
721
|
-
|
|
653
|
+
To set up the project for local development:
|
|
654
|
+
|
|
655
|
+
1. **Clone the repository:**
|
|
722
656
|
```bash
|
|
723
657
|
git clone https://github.com/asaidimu/erp-utils.git
|
|
724
|
-
cd erp-utils/src/artifacts
|
|
658
|
+
cd erp-utils/src/artifacts
|
|
725
659
|
```
|
|
726
|
-
2. **Install dependencies
|
|
660
|
+
2. **Install dependencies:**
|
|
727
661
|
```bash
|
|
728
|
-
|
|
729
|
-
# or
|
|
730
|
-
|
|
662
|
+
npm install
|
|
663
|
+
# or
|
|
664
|
+
yarn install
|
|
731
665
|
```
|
|
732
|
-
3. **Build (if
|
|
666
|
+
3. **Build the project (if applicable, though typically handled by IDE/watch mode):**
|
|
733
667
|
```bash
|
|
734
|
-
|
|
668
|
+
npm run build # Or `tsc` if not defined in package.json scripts
|
|
735
669
|
```
|
|
736
670
|
|
|
737
671
|
### Scripts
|
|
738
672
|
|
|
739
|
-
The `package.json`
|
|
673
|
+
The `package.json` defines the following scripts:
|
|
740
674
|
|
|
741
|
-
* `
|
|
742
|
-
* `
|
|
743
|
-
* `
|
|
744
|
-
* `bun run test:browser`: Runs tests in a browser environment using Playwright.
|
|
745
|
-
* `bun run build`: Compiles the TypeScript source code into JavaScript.
|
|
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.
|
|
746
678
|
|
|
747
679
|
### Testing
|
|
748
680
|
|
|
749
|
-
|
|
681
|
+
The project uses `vitest` for testing.
|
|
750
682
|
|
|
751
|
-
* To run all tests:
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
* To run tests in watch mode:
|
|
756
|
-
```bash
|
|
757
|
-
bun run test --watch
|
|
758
|
-
```
|
|
759
|
-
* To run tests in a browser environment:
|
|
760
|
-
```bash
|
|
761
|
-
bun run test:browser
|
|
762
|
-
```
|
|
763
|
-
* Ensure all new features or bug fixes are accompanied by appropriate unit and/or integration tests.
|
|
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.
|
|
764
687
|
|
|
765
688
|
### Contributing Guidelines
|
|
766
689
|
|
|
767
|
-
We
|
|
768
|
-
Please refer to the main repository's [CONTRIBUTING.md](https://github.com/asaidimu/erp-utils/blob/main/CONTRIBUTING.md) for detailed guidelines on:
|
|
690
|
+
We welcome contributions! Please follow these guidelines:
|
|
769
691
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
692
|
+
1. **Fork the repository** and create your branch from `main`.
|
|
693
|
+
2. **Ensure code quality**: Write clean, readable TypeScript code. Adhere to existing coding style (ESLint and Prettier are typically configured in parent project).
|
|
694
|
+
3. **Tests**: All new features and bug fixes should be accompanied by appropriate unit or integration tests. Ensure existing tests pass.
|
|
695
|
+
4. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history. (e.g., `feat: add new artifact scope`, `fix: resolve circular dependency issue`).
|
|
696
|
+
5. **Pull Requests**:
|
|
697
|
+
* Open a detailed Pull Request describing the changes, new features, or bug fixes.
|
|
698
|
+
* Reference any related issues.
|
|
699
|
+
* Ensure your branch is up-to-date with `main`.
|
|
774
700
|
|
|
775
701
|
### Issue Reporting
|
|
776
702
|
|
|
777
|
-
|
|
778
|
-
Provide as much detail as possible, including steps to reproduce, expected behavior, and your environment.
|
|
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.
|
|
779
704
|
|
|
780
705
|
---
|
|
781
706
|
|
|
782
|
-
##
|
|
707
|
+
## ℹ️ Additional Information
|
|
783
708
|
|
|
784
709
|
### Troubleshooting
|
|
785
710
|
|
|
786
|
-
* **`ArtifactNotFoundError`**: This
|
|
787
|
-
* **`CircularDependencyError`**:
|
|
788
|
-
*
|
|
789
|
-
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
* **`WatcherDisposedError`**: You are attempting to access a watcher's state (`watcher.get()`) after `watcher.dispose()` has been called. Ensure you unsubscribe from all callbacks (`unsubscribe()`) and dispose of watchers when no longer needed (`watcher.dispose()`), and avoid using them afterward.
|
|
711
|
+
* **`ArtifactNotFoundError`**: This means you're trying to `resolve` or `watch` an artifact that hasn't been `register`ed. Double-check your artifact keys and ensure registration happens before resolution.
|
|
712
|
+
* **`CircularDependencyError`**: This occurs when your artifact graph forms a loop (e.g., A depends on B, B depends on A). The `debugInfo()` output and the error message's path can help you identify the cycle. Redesign your dependencies to break the cycle.
|
|
713
|
+
* **`TimeoutError`**: Your artifact factory took longer than the specified `timeout` during registration. This can indicate long-running sync operations, slow async dependencies, or an infinite loop. Increase the timeout or optimize your factory.
|
|
714
|
+
* **Unexpected Rebuilds/No Rebuilds**:
|
|
715
|
+
* Use `container.debugInfo()` to inspect artifact statuses and `stateDependencies`/`dependencies`.
|
|
716
|
+
* Ensure your `ctx.select()` selectors correctly identify the state slices you intend to react to.
|
|
717
|
+
* Check `debounce` settings if an artifact seems to rebuild too frequently or too slowly.
|
|
718
|
+
* Verify `onCleanup` and `onDispose` hooks are being called as expected to rule out resource leaks.
|
|
795
719
|
|
|
796
720
|
### FAQ
|
|
797
721
|
|
|
798
|
-
* **What
|
|
799
|
-
|
|
800
|
-
*
|
|
801
|
-
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
722
|
+
* **What's the difference between `Singleton` and `Transient` scopes?**
|
|
723
|
+
* `Singleton`: Only one instance of the artifact is ever created. Subsequent `resolve` calls return the same instance. This is suitable for services, configurations, or shared resources. They support state dependencies, `onCleanup`/`onDispose`, and `stream`.
|
|
724
|
+
* `Transient`: A new instance is created every time the artifact is resolved. Useful for ephemeral objects that should not be shared or cached. They do not support `stream` or persistent `onCleanup`/`onDispose` (though a single `cleanup` can be returned by `resolve`).
|
|
725
|
+
* **When should I use `resolve` versus `require`?**
|
|
726
|
+
* Use `resolve` when you need to defensively handle potential errors or pending states of an artifact. It returns a `ResolvedArtifact` object that allows explicit checks (`.ready`, `.error`).
|
|
727
|
+
* Use `require` when you are confident the artifact will resolve successfully and prefer to get the instance directly, allowing errors to propagate as exceptions. This simplifies code where error handling is done at a higher level.
|
|
728
|
+
* **How does `debounce` work for invalidation?**
|
|
729
|
+
`debounce` adds a delay (in milliseconds) before an artifact rebuilds after its dependencies change. If multiple dependency changes occur within this debounce period, the rebuild is aggregated into a single event, preventing excessive rapid rebuilds. `container.invalidate(key, true)` can bypass this debounce.
|
|
730
|
+
* **What is the purpose of `onCleanup` vs. `onDispose`?**
|
|
731
|
+
* `onCleanup`: Tied to the *current instance's lifecycle*. It runs when a `Singleton` artifact's instance is replaced (e.g., due to an invalidation and rebuild), or when a `Transient` instance is returned and subsequently discarded. Use for resources specific to that particular instance.
|
|
732
|
+
* `onDispose`: Tied to the *artifact's registration lifecycle*. It runs only when the artifact is permanently `unregister`ed from the container or when the container itself is `dispose`d. Use for resources that should persist across instance rebuilds but be released when the artifact itself is no longer managed.
|
|
807
733
|
|
|
808
|
-
### Changelog
|
|
734
|
+
### Changelog/Roadmap
|
|
809
735
|
|
|
810
|
-
|
|
811
|
-
|
|
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.
|
|
812
738
|
|
|
813
739
|
### License
|
|
814
740
|
|
|
@@ -816,7 +742,4 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
|
|
|
816
742
|
|
|
817
743
|
### Acknowledgments
|
|
818
744
|
|
|
819
|
-
|
|
820
|
-
* Leverages foundational packages like [`@asaidimu/utils-store`](https://www.npmjs.com/package/@asaidimu/utils-store) and `@asaidimu/events` within the `erp-utils` monorepo.
|
|
821
|
-
* Uses [Semantic Release](https://semantic-release.gitbook.io/semantic-release/) for automated versioning and publishing.
|
|
822
|
-
* Utilizes [Vitest](https://vitest.dev/) for fast and reliable testing.
|
|
745
|
+
This library is part of the `@asaidimu/erp-utils` monorepository and integrates closely with `@core/store/types` (specifically, the `DataStore` interface) for reactive state management.
|