@asaidimu/utils-artifacts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +799 -0
- package/index.d.mts +613 -0
- package/index.d.ts +613 -0
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
# @asaidimu/utils-artifacts
|
|
2
|
+
|
|
3
|
+
A powerful, reactive dependency injection container for managing application artifacts and their lifecycles. It provides a robust framework for building modular, maintainable, and highly responsive applications by decoupling components and managing their state-driven dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@asaidimu/utils-artifacts)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/asaidimu/utils/actions?query=workflow%3ACI)
|
|
8
|
+
|
|
9
|
+
## 🚀 Quick Links
|
|
10
|
+
|
|
11
|
+
* [Overview & Features](#-overview--features)
|
|
12
|
+
* [Installation & Setup](#-installation--setup)
|
|
13
|
+
* [Usage Documentation](#-usage-documentation)
|
|
14
|
+
* [Basic Artifacts (Singleton & Transient)](#basic-artifacts-singleton--transient)
|
|
15
|
+
* [Reactive State Dependencies](#reactive-state-dependencies)
|
|
16
|
+
* [Artifact-to-Artifact Dependencies](#artifact-to-artifact-dependencies)
|
|
17
|
+
* [Lifecycle Hooks: `onCleanup` & `onDispose`](#lifecycle-hooks-oncleanup--ondispose)
|
|
18
|
+
* [Watching Artifacts for Changes](#watching-artifacts-for-changes)
|
|
19
|
+
* [Yielding Intermediate Values](#yielding-intermediate-values)
|
|
20
|
+
* [Hierarchical Containers](#hierarchical-containers)
|
|
21
|
+
* [Debugging Information](#debugging-information)
|
|
22
|
+
* [Project Architecture](#-project-architecture)
|
|
23
|
+
* [Development & Contributing](#-development--contributing)
|
|
24
|
+
* [Additional Information](#-additional-information)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
⚠️ **Beta Warning**
|
|
29
|
+
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!
|
|
30
|
+
|
|
31
|
+
## 📚 Overview & Features
|
|
32
|
+
|
|
33
|
+
`@asaidimu/utils-artifacts` is a sophisticated dependency injection (DI) container designed for modern TypeScript applications. It allows you to register "artifacts" – which can be any JavaScript object, function, or value – and manage their creation, lifecycle, and dependencies. What sets it apart is its reactive nature: artifacts can declare dependencies on both other artifacts and specific slices of a global application state (managed by an external `DataStore` like `@asaidimu/utils-store`). When these declared dependencies change, the container automatically invalidates and rebuilds the affected artifacts, propagating updates through the dependency graph efficiently, with support for debouncing to prevent excessive rebuilds.
|
|
34
|
+
|
|
35
|
+
This package is ideal for building complex, event-driven systems, front-end applications, or backend services where state changes need to trigger logical recalculations or UI updates in a structured and testable manner. It promotes modularity, testability, and maintainability by centralizing dependency management and automating reactive updates.
|
|
36
|
+
|
|
37
|
+
### ✨ Key Features
|
|
38
|
+
|
|
39
|
+
* **Reactive Dependency Injection**: Automatically invalidates and rebuilds artifacts when their declared dependencies (other artifacts or global state slices) change.
|
|
40
|
+
* **Singleton & Transient Scopes**: Control whether an artifact is instantiated once and reused (`Singleton`) or created fresh on every request (`Transient`).
|
|
41
|
+
* **Hierarchical Containers**: Create child containers that can inherit and resolve artifacts from their parents, enabling scoped dependency trees.
|
|
42
|
+
* **State-Driven Invalidation with Debouncing**: Integrates with a `DataStore` to track state dependencies, triggering artifact rebuilds only when relevant state paths change, with configurable debouncing.
|
|
43
|
+
* **Fine-grained Dependency Tracking**: Explicitly declare dependencies on other artifacts using `ctx.resolve()` and on state slices using `ctx.select()`.
|
|
44
|
+
* **Robust Error Handling**: Built-in detection and specific error types for circular dependencies, missing artifacts, and illegal operations, distinguishing between `system` (structural) and `external` (runtime) errors.
|
|
45
|
+
* **Lifecycle Hooks**: Register `onCleanup` callbacks for instance-specific resource release and `onDispose` for final resource teardown.
|
|
46
|
+
* **Asynchronous Artifact Resolution**: Supports `async`/`await` in artifact factories, with configurable retries for external failures and timeouts.
|
|
47
|
+
* **`ArtifactWatcher` for Reactive Observation**: Observe changes to an artifact's resolved value over time, subscribing to updates without repeatedly resolving.
|
|
48
|
+
* **Live Updates via `yield()`**: Singleton artifacts can proactively update their value from within their factory, immediately notifying dependents and watchers.
|
|
49
|
+
* **Comprehensive Debugging Info**: `getDebugInfo()` provides a snapshot of the container's state, including artifact statuses, dependencies, and dependents.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📦 Installation & Setup
|
|
54
|
+
|
|
55
|
+
### Prerequisites
|
|
56
|
+
|
|
57
|
+
* Node.js (v18 or higher recommended)
|
|
58
|
+
* A package manager like Bun, npm, or Yarn.
|
|
59
|
+
* An implementation of the `DataStore` interface for reactive state management. We highly recommend using [`@asaidimu/utils-store`](https://www.npmjs.com/package/@asaidimu/utils-store).
|
|
60
|
+
|
|
61
|
+
### Installation Steps
|
|
62
|
+
|
|
63
|
+
Install the package using your preferred package manager:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# With Bun
|
|
67
|
+
bun add @asaidimu/utils-artifacts
|
|
68
|
+
|
|
69
|
+
# With npm
|
|
70
|
+
npm install @asaidimu/utils-artifacts
|
|
71
|
+
|
|
72
|
+
# With Yarn
|
|
73
|
+
yarn add @asaidimu/utils-artifacts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Configuration
|
|
77
|
+
|
|
78
|
+
To use the `ArtifactContainer`, you need to initialize it with an object that provides `get` and `watch` methods conforming to the `DataStore` interface. This allows the container to read the current application state and subscribe to state changes for reactive invalidation.
|
|
79
|
+
|
|
80
|
+
If you're using `@asaidimu/utils-store` as your `DataStore`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
84
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store'; // Assuming this provides DataStore interface
|
|
85
|
+
|
|
86
|
+
interface AppState {
|
|
87
|
+
config: { theme: string; debugMode: boolean };
|
|
88
|
+
user: { id: string; name: string };
|
|
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
|
+
};
|
|
97
|
+
|
|
98
|
+
const store = new ReactiveDataStore<AppState>(initialAppState);
|
|
99
|
+
|
|
100
|
+
// Initialize the ArtifactContainer
|
|
101
|
+
const container = new ArtifactContainer<Record<string, any>, AppState>(store);
|
|
102
|
+
|
|
103
|
+
// Now you can start registering artifacts
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Verification
|
|
107
|
+
|
|
108
|
+
You can verify the installation by trying to import and instantiate the `ArtifactContainer`:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { ArtifactContainer } from '@asaidimu/utils-artifacts';
|
|
112
|
+
// Assuming a mock or real DataStore is available
|
|
113
|
+
const mockStore = {
|
|
114
|
+
get: () => ({}),
|
|
115
|
+
watch: () => () => {},
|
|
116
|
+
};
|
|
117
|
+
const container = new ArtifactContainer(mockStore);
|
|
118
|
+
console.log('ArtifactContainer initialized successfully!');
|
|
119
|
+
// If no errors, the installation is successful.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 💡 Usage Documentation
|
|
125
|
+
|
|
126
|
+
### Basic Artifacts (Singleton & Transient)
|
|
127
|
+
|
|
128
|
+
Artifacts can be registered with different scopes, controlling their lifecycle.
|
|
129
|
+
|
|
130
|
+
#### Singleton Scope
|
|
131
|
+
|
|
132
|
+
A singleton artifact is created only once. Subsequent `resolve` calls return the same instance.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
136
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
137
|
+
|
|
138
|
+
const store = new ReactiveDataStore({});
|
|
139
|
+
const container = new ArtifactContainer(store);
|
|
140
|
+
|
|
141
|
+
let buildCount = 0;
|
|
142
|
+
|
|
143
|
+
container.register({
|
|
144
|
+
key: 'mySingletonService',
|
|
145
|
+
factory: () => {
|
|
146
|
+
buildCount++;
|
|
147
|
+
console.log('Building mySingletonService...');
|
|
148
|
+
return {
|
|
149
|
+
name: 'SingletonService',
|
|
150
|
+
version: '1.0.0'
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
scope: ArtifactScope.Singleton,
|
|
154
|
+
lazy: false, // Build immediately on registration
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
async function runSingletonExample() {
|
|
158
|
+
const service1 = await container.resolve('mySingletonService');
|
|
159
|
+
const service2 = await container.resolve('mySingletonService');
|
|
160
|
+
|
|
161
|
+
console.log('Service 1:', service1.instance);
|
|
162
|
+
console.log('Service 2:', service2.instance);
|
|
163
|
+
|
|
164
|
+
console.assert(buildCount === 1, 'Singleton factory should be called once');
|
|
165
|
+
console.assert(service1.instance === service2.instance, 'Both resolutions should return the same instance');
|
|
166
|
+
console.assert(service1.ready, 'Singleton should be ready');
|
|
167
|
+
|
|
168
|
+
// Output:
|
|
169
|
+
// Building mySingletonService...
|
|
170
|
+
// Service 1: { name: 'SingletonService', version: '1.0.0' }
|
|
171
|
+
// Service 2: { name: 'SingletonService', version: '1.0.0' }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
runSingletonExample();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Transient Scope
|
|
178
|
+
|
|
179
|
+
A transient artifact creates a new instance every time it is resolved.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
183
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
184
|
+
|
|
185
|
+
const store = new ReactiveDataStore({});
|
|
186
|
+
const container = new ArtifactContainer(store);
|
|
187
|
+
|
|
188
|
+
container.register({
|
|
189
|
+
key: 'myTransientFactory',
|
|
190
|
+
factory: () => {
|
|
191
|
+
console.log('Building myTransientFactory...');
|
|
192
|
+
return {
|
|
193
|
+
id: Math.random().toString(36).substring(7),
|
|
194
|
+
timestamp: Date.now()
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
scope: ArtifactScope.Transient,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
async function runTransientExample() {
|
|
201
|
+
const instance1 = await container.resolve('myTransientFactory');
|
|
202
|
+
const instance2 = await container.resolve('myTransientFactory');
|
|
203
|
+
|
|
204
|
+
console.log('Instance 1:', instance1.instance);
|
|
205
|
+
console.log('Instance 2:', instance2.instance);
|
|
206
|
+
|
|
207
|
+
console.assert(instance1.instance !== instance2.instance, 'Transient should return different instances');
|
|
208
|
+
console.assert(instance1.ready && instance2.ready, 'Transient instances should be ready immediately');
|
|
209
|
+
|
|
210
|
+
// Output:
|
|
211
|
+
// Building myTransientFactory...
|
|
212
|
+
// Building myTransientFactory...
|
|
213
|
+
// Instance 1: { id: '...', timestamp: ... }
|
|
214
|
+
// Instance 2: { id: '...', timestamp: ... }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
runTransientExample();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Reactive State Dependencies
|
|
221
|
+
|
|
222
|
+
Artifacts can react to changes in your global `DataStore` state by using `ctx.select()` within their factory's `use` block.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
226
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
227
|
+
|
|
228
|
+
interface AppState {
|
|
229
|
+
theme: 'dark' | 'light';
|
|
230
|
+
notificationsEnabled: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const store = new ReactiveDataStore<AppState>({
|
|
234
|
+
theme: 'dark',
|
|
235
|
+
notificationsEnabled: true,
|
|
236
|
+
});
|
|
237
|
+
const container = new ArtifactContainer<any, AppState>(store);
|
|
238
|
+
|
|
239
|
+
let uiComponentBuilds = 0;
|
|
240
|
+
container.register({
|
|
241
|
+
key: 'uiConfiguration',
|
|
242
|
+
factory: async ({ use }) => {
|
|
243
|
+
uiComponentBuilds++;
|
|
244
|
+
const theme = await use((ctx) => ctx.select((state) => state.theme));
|
|
245
|
+
const notifications = await use((ctx) => ctx.select((state) => state.notificationsEnabled));
|
|
246
|
+
|
|
247
|
+
console.log(`UI Config Build ${uiComponentBuilds}: Theme=${theme}, Notifications=${notifications}`);
|
|
248
|
+
return { theme, notifications };
|
|
249
|
+
},
|
|
250
|
+
scope: ArtifactScope.Singleton,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
async function runReactiveStateExample() {
|
|
254
|
+
// Initial resolution
|
|
255
|
+
let uiConfig = await container.resolve('uiConfiguration');
|
|
256
|
+
console.assert(uiConfig.instance?.theme === 'dark', 'Initial theme should be dark');
|
|
257
|
+
console.assert(uiComponentBuilds === 1, 'Initial build count should be 1');
|
|
258
|
+
|
|
259
|
+
// Simulate state change
|
|
260
|
+
console.log('\n--- Changing theme to light ---');
|
|
261
|
+
await store.set({ theme: 'light' });
|
|
262
|
+
|
|
263
|
+
// Resolve again - artifact should have been invalidated and rebuilt
|
|
264
|
+
uiConfig = await container.resolve('uiConfiguration');
|
|
265
|
+
console.assert(uiConfig.instance?.theme === 'light', 'Updated theme should be light');
|
|
266
|
+
console.assert(uiComponentBuilds === 2, 'Build count should increment after state change');
|
|
267
|
+
|
|
268
|
+
// Simulate another state change (different property)
|
|
269
|
+
console.log('\n--- Disabling notifications ---');
|
|
270
|
+
await store.set({ notificationsEnabled: false });
|
|
271
|
+
|
|
272
|
+
// Resolve again
|
|
273
|
+
uiConfig = await container.resolve('uiConfiguration');
|
|
274
|
+
console.assert(uiConfig.instance?.notificationsEnabled === false, 'Notifications should be false');
|
|
275
|
+
console.assert(uiComponentBuilds === 3, 'Build count should increment after another state change');
|
|
276
|
+
|
|
277
|
+
// Output:
|
|
278
|
+
// UI Config Build 1: Theme=dark, Notifications=true
|
|
279
|
+
//
|
|
280
|
+
// --- Changing theme to light ---
|
|
281
|
+
// UI Config Build 2: Theme=light, Notifications=true
|
|
282
|
+
//
|
|
283
|
+
// --- Disabling notifications ---
|
|
284
|
+
// UI Config Build 3: Theme=light, Notifications=false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
runReactiveStateExample();
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Artifact-to-Artifact Dependencies
|
|
291
|
+
|
|
292
|
+
Artifacts can depend on other artifacts using `ctx.resolve()` within their factory's `use` block. Changes to a dependency will invalidate the dependent artifact.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
296
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
297
|
+
|
|
298
|
+
const store = new ReactiveDataStore({});
|
|
299
|
+
const container = new ArtifactContainer(store);
|
|
300
|
+
|
|
301
|
+
let dbConnectionBuilds = 0;
|
|
302
|
+
container.register({
|
|
303
|
+
key: 'dbConnection',
|
|
304
|
+
factory: async () => {
|
|
305
|
+
dbConnectionBuilds++;
|
|
306
|
+
console.log(`Establishing DB Connection (${dbConnectionBuilds})...`);
|
|
307
|
+
await new Promise(res => setTimeout(res, 50)); // Simulate async work
|
|
308
|
+
return { client: 'PostgreSQL', status: 'connected' };
|
|
309
|
+
},
|
|
310
|
+
scope: ArtifactScope.Singleton,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
let userRepositoryBuilds = 0;
|
|
314
|
+
container.register({
|
|
315
|
+
key: 'userRepository',
|
|
316
|
+
factory: async ({ use }) => {
|
|
317
|
+
userRepositoryBuilds++;
|
|
318
|
+
console.log(`Building UserRepository (${userRepositoryBuilds})...`);
|
|
319
|
+
const db = await use((ctx) => ctx.resolve('dbConnection'));
|
|
320
|
+
return {
|
|
321
|
+
dbClient: db.instance?.client,
|
|
322
|
+
findUser: (id: string) => `User ${id} from ${db.instance?.client}`
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
scope: ArtifactScope.Singleton,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
async function runArtifactDependencyExample() {
|
|
329
|
+
// Initial resolution of UserRepository, which will resolve dbConnection first
|
|
330
|
+
let userRepo = await container.resolve('userRepository');
|
|
331
|
+
console.assert(dbConnectionBuilds === 1, 'DB Connection built once');
|
|
332
|
+
console.assert(userRepositoryBuilds === 1, 'User Repository built once');
|
|
333
|
+
console.log('User Repo initialized:', userRepo.instance?.findUser('1'));
|
|
334
|
+
|
|
335
|
+
// Manually invalidate the dbConnection. This will trigger a rebuild of both.
|
|
336
|
+
console.log('\n--- Invalidating DB Connection ---');
|
|
337
|
+
const dbConnection = await container.resolve('dbConnection');
|
|
338
|
+
await dbConnection.invalidate(true); // `true` forces immediate rebuild
|
|
339
|
+
|
|
340
|
+
// Resolve UserRepository again - it should have been rebuilt
|
|
341
|
+
userRepo = await container.resolve('userRepository');
|
|
342
|
+
console.assert(dbConnectionBuilds === 2, 'DB Connection rebuilt');
|
|
343
|
+
console.assert(userRepositoryBuilds === 2, 'User Repository rebuilt due to DB change');
|
|
344
|
+
console.log('User Repo after invalidation:', userRepo.instance?.findUser('2'));
|
|
345
|
+
|
|
346
|
+
// Output:
|
|
347
|
+
// Establishing DB Connection (1)...
|
|
348
|
+
// Building UserRepository (1)...
|
|
349
|
+
// User Repo initialized: User 1 from PostgreSQL
|
|
350
|
+
//
|
|
351
|
+
// --- Invalidating DB Connection ---
|
|
352
|
+
// Establishing DB Connection (2)...
|
|
353
|
+
// Building UserRepository (2)...
|
|
354
|
+
// User Repo after invalidation: User 2 from PostgreSQL
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
runArtifactDependencyExample();
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Lifecycle Hooks: `onCleanup` & `onDispose`
|
|
361
|
+
|
|
362
|
+
Artifacts can register cleanup functions to manage resources.
|
|
363
|
+
|
|
364
|
+
* `onCleanup`: Executed *before* a new instance of a `Singleton` artifact is built (due to invalidation) or when the artifact is explicitly unregistered. Ideal for releasing resources tied to a specific instance.
|
|
365
|
+
* `onDispose`: Executed *only* when the artifact is unregistered or the container itself is disposed. Ideal for final, permanent resource release.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
369
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
370
|
+
|
|
371
|
+
const store = new ReactiveDataStore({});
|
|
372
|
+
const container = new ArtifactContainer(store);
|
|
373
|
+
|
|
374
|
+
let currentInterval: any;
|
|
375
|
+
|
|
376
|
+
container.register({
|
|
377
|
+
key: 'ticker',
|
|
378
|
+
factory: ({ onCleanup, onDispose }) => {
|
|
379
|
+
console.log('Ticker artifact created.');
|
|
380
|
+
let count = 0;
|
|
381
|
+
currentInterval = setInterval(() => {
|
|
382
|
+
count++;
|
|
383
|
+
console.log(`Tick: ${count}`);
|
|
384
|
+
}, 1000);
|
|
385
|
+
|
|
386
|
+
onCleanup(() => {
|
|
387
|
+
console.log('Cleaning up old Ticker instance (stopping interval)...');
|
|
388
|
+
clearInterval(currentInterval);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
onDispose(() => {
|
|
392
|
+
console.log('Disposing Ticker artifact (final cleanup)...');
|
|
393
|
+
// Additional final cleanup could go here
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return { start: Date.now() };
|
|
397
|
+
},
|
|
398
|
+
scope: ArtifactScope.Singleton,
|
|
399
|
+
lazy: false, // Eagerly build
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
async function runLifecycleHooksExample() {
|
|
403
|
+
await container.resolve('ticker');
|
|
404
|
+
console.log('Ticker running for 3 seconds...');
|
|
405
|
+
await new Promise(res => setTimeout(res, 3000));
|
|
406
|
+
|
|
407
|
+
console.log('\n--- Invalidating Ticker (will trigger onCleanup and rebuild) ---');
|
|
408
|
+
const ticker = await container.resolve('ticker'); // Get current to invalidate
|
|
409
|
+
await ticker.invalidate(true); // Force rebuild
|
|
410
|
+
console.log('Ticker rebuilt. Running for 2 more seconds...');
|
|
411
|
+
await new Promise(res => setTimeout(res, 2000));
|
|
412
|
+
|
|
413
|
+
console.log('\n--- Unregistering Ticker (will trigger onDispose) ---');
|
|
414
|
+
await container.unregister('ticker');
|
|
415
|
+
console.log('Ticker unregistered.');
|
|
416
|
+
|
|
417
|
+
// Output will show 'Cleaning up old Ticker instance' on invalidation,
|
|
418
|
+
// and 'Disposing Ticker artifact' on unregister.
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
runLifecycleHooksExample();
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Watching Artifacts for Changes
|
|
425
|
+
|
|
426
|
+
The `watch()` method provides a reactive way to observe an artifact's value without repeatedly calling `resolve()`. This is particularly useful for UI frameworks or reactive data flows.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
430
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
431
|
+
|
|
432
|
+
interface AppState {
|
|
433
|
+
counter: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const store = new ReactiveDataStore<AppState>({ counter: 0 });
|
|
437
|
+
const container = new ArtifactContainer<any, AppState>(store);
|
|
438
|
+
|
|
439
|
+
container.register({
|
|
440
|
+
key: 'reactiveCounter',
|
|
441
|
+
factory: async ({ use }) => {
|
|
442
|
+
const count = await use((ctx) => ctx.select((state) => state.counter));
|
|
443
|
+
return `Current count: ${count}`;
|
|
444
|
+
},
|
|
445
|
+
scope: ArtifactScope.Singleton,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
async function runWatcherExample() {
|
|
449
|
+
console.log('Setting up watcher...');
|
|
450
|
+
const watcher = container.watch('reactiveCounter');
|
|
451
|
+
|
|
452
|
+
let unsubscribe = watcher.subscribe(() => {
|
|
453
|
+
const resolved = watcher.get();
|
|
454
|
+
if (resolved.ready) {
|
|
455
|
+
console.log('Watcher received update:', resolved.instance);
|
|
456
|
+
} else if (resolved.error) {
|
|
457
|
+
console.error('Watcher received error:', resolved.error);
|
|
458
|
+
} else {
|
|
459
|
+
console.log('Watcher received pending update (artifact not ready yet).');
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Initial state should trigger a notification (pending, then ready)
|
|
464
|
+
await new Promise(res => setTimeout(res, 10)); // Give watcher time to resolve
|
|
465
|
+
|
|
466
|
+
console.log('\n--- Incrementing counter ---');
|
|
467
|
+
await store.set({ counter: 1 });
|
|
468
|
+
await new Promise(res => setTimeout(res, 10));
|
|
469
|
+
|
|
470
|
+
console.log('\n--- Incrementing counter again ---');
|
|
471
|
+
await store.set({ counter: 2 });
|
|
472
|
+
await new Promise(res => setTimeout(res, 10));
|
|
473
|
+
|
|
474
|
+
console.log('\n--- Disposing watcher ---');
|
|
475
|
+
unsubscribe(); // Unsubscribe first
|
|
476
|
+
await watcher.dispose(); // Then dispose the watcher itself
|
|
477
|
+
|
|
478
|
+
// Output:
|
|
479
|
+
// Setting up watcher...
|
|
480
|
+
// Watcher received pending update (artifact not ready yet).
|
|
481
|
+
// Watcher received update: Current count: 0
|
|
482
|
+
//
|
|
483
|
+
// --- Incrementing counter ---
|
|
484
|
+
// Watcher received update: Current count: 1
|
|
485
|
+
//
|
|
486
|
+
// --- Incrementing counter again ---
|
|
487
|
+
// Watcher received update: Current count: 2
|
|
488
|
+
//
|
|
489
|
+
// --- Disposing watcher ---
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
runWatcherExample();
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Yielding Intermediate Values
|
|
496
|
+
|
|
497
|
+
For long-running or multi-stage artifact factories, `ctx.yield()` allows a Singleton artifact to publish intermediate values. This updates the artifact's current instance and notifies dependents/watchers without fully completing the factory execution.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
501
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
502
|
+
|
|
503
|
+
const store = new ReactiveDataStore({});
|
|
504
|
+
const container = new ArtifactContainer(store);
|
|
505
|
+
|
|
506
|
+
container.register({
|
|
507
|
+
key: 'longProcessArtifact',
|
|
508
|
+
factory: async ({ yield: publish }) => {
|
|
509
|
+
async function process() {
|
|
510
|
+
console.log('Starting long process...');
|
|
511
|
+
|
|
512
|
+
publish('Step 1: Initializing...');
|
|
513
|
+
await new Promise(res => setTimeout(res, 100));
|
|
514
|
+
|
|
515
|
+
publish('Step 2: Loading data...');
|
|
516
|
+
await new Promise(res => setTimeout(res, 100));
|
|
517
|
+
|
|
518
|
+
publish('Step 3: Processing data...');
|
|
519
|
+
await new Promise(res => setTimeout(res, 100));
|
|
520
|
+
|
|
521
|
+
console.log('Long process complete.');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
process()
|
|
525
|
+
return 'Created factory...';
|
|
526
|
+
},
|
|
527
|
+
scope: ArtifactScope.Singleton,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
async function runYieldExample() {
|
|
531
|
+
const watcher = container.watch('longProcessArtifact');
|
|
532
|
+
const unsubscribe = watcher.subscribe(() => {
|
|
533
|
+
const resolved = watcher.get();
|
|
534
|
+
if (resolved.instance) {
|
|
535
|
+
console.log('Watcher observed yield:', resolved.instance);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await container.resolve('longProcessArtifact'); // Trigger the factory
|
|
540
|
+
console.log('Factory finished.');
|
|
541
|
+
|
|
542
|
+
unsubscribe();
|
|
543
|
+
await watcher.dispose();
|
|
544
|
+
|
|
545
|
+
// Output will show intermediate values from `yield` as they occur,
|
|
546
|
+
// then the final result once the factory completes.
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
runYieldExample();
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Hierarchical Containers
|
|
553
|
+
|
|
554
|
+
Create child containers to manage dependencies in a nested, scoped manner. Child containers can resolve artifacts registered in their parent.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
558
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
559
|
+
|
|
560
|
+
const store = new ReactiveDataStore({});
|
|
561
|
+
const parentContainer = new ArtifactContainer(store);
|
|
562
|
+
|
|
563
|
+
parentContainer.register({
|
|
564
|
+
key: 'globalConfig',
|
|
565
|
+
factory: () => ({ logLevel: 'info', serverUrl: 'api.example.com' }),
|
|
566
|
+
scope: ArtifactScope.Singleton,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const childContainer = parentContainer.createChild();
|
|
570
|
+
|
|
571
|
+
childContainer.register({
|
|
572
|
+
key: 'featureService',
|
|
573
|
+
factory: async ({ use }) => {
|
|
574
|
+
const config = await use((ctx) => ctx.resolve('globalConfig')); // Resolves from parent
|
|
575
|
+
return {
|
|
576
|
+
name: 'FeatureService',
|
|
577
|
+
apiUrl: `${config.instance?.serverUrl}/feature`
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
scope: ArtifactScope.Singleton,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
async function runHierarchicalExample() {
|
|
584
|
+
const globalConfig = await parentContainer.resolve('globalConfig');
|
|
585
|
+
const featureService = await childContainer.resolve('featureService');
|
|
586
|
+
|
|
587
|
+
console.log('Global Config from parent:', globalConfig.instance);
|
|
588
|
+
console.log('Feature Service from child:', featureService.instance);
|
|
589
|
+
|
|
590
|
+
console.assert(globalConfig.instance?.logLevel === 'info', 'Parent artifact resolved');
|
|
591
|
+
console.assert(featureService.instance?.apiUrl === 'api.example.com/feature', 'Child resolved parent artifact');
|
|
592
|
+
|
|
593
|
+
// Output:
|
|
594
|
+
// Global Config from parent: { logLevel: 'info', serverUrl: 'api.example.com' }
|
|
595
|
+
// Feature Service from child: { name: 'FeatureService', apiUrl: 'api.example.com/feature' }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
runHierarchicalExample();
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Debugging Information
|
|
602
|
+
|
|
603
|
+
Use `getDebugInfo()` to inspect the current state of artifacts within a container, including their status, dependencies, and dependents.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-artifacts';
|
|
607
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
608
|
+
|
|
609
|
+
const store = new ReactiveDataStore({});
|
|
610
|
+
const container = new ArtifactContainer(store);
|
|
611
|
+
|
|
612
|
+
container.register({
|
|
613
|
+
key: 'serviceA',
|
|
614
|
+
factory: () => 'Service A Instance',
|
|
615
|
+
scope: ArtifactScope.Singleton,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
container.register({
|
|
619
|
+
key: 'serviceB',
|
|
620
|
+
factory: async ({ use }) => {
|
|
621
|
+
await use((ctx) => ctx.resolve('serviceA'));
|
|
622
|
+
return 'Service B Instance';
|
|
623
|
+
},
|
|
624
|
+
scope: ArtifactScope.Singleton,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
async function runDebugInfoExample() {
|
|
628
|
+
await container.resolve('serviceB'); // Resolve to build dependencies
|
|
629
|
+
|
|
630
|
+
const debugInfo = container.getDebugInfo();
|
|
631
|
+
console.log('--- Artifact Debug Information ---');
|
|
632
|
+
debugInfo.forEach(node => {
|
|
633
|
+
console.log(`ID: ${node.id}`);
|
|
634
|
+
console.log(` Scope: ${node.scope}`);
|
|
635
|
+
console.log(` Status: ${node.status}`);
|
|
636
|
+
console.log(` Dependencies: ${node.dependencies.join(', ') || 'None'}`);
|
|
637
|
+
console.log(` Dependents: ${node.dependents.join(', ') || 'None'}`);
|
|
638
|
+
console.log(` Render Count: ${node.renderCount}`);
|
|
639
|
+
console.log('---');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Example output for 'serviceB':
|
|
643
|
+
// ID: serviceB
|
|
644
|
+
// Scope: singleton
|
|
645
|
+
// Status: active
|
|
646
|
+
// Dependencies: serviceA
|
|
647
|
+
// Dependents: None
|
|
648
|
+
// Render Count: 1
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
runDebugInfoExample();
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## 🏛️ Project Architecture
|
|
657
|
+
|
|
658
|
+
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).
|
|
659
|
+
|
|
660
|
+
### Core Components
|
|
661
|
+
|
|
662
|
+
* **`ArtifactContainer`**: The heart of the system. It manages the registration, resolution, lifecycle (Singleton/Transient), and reactive updates of all artifacts. It can also create child containers to form a hierarchy.
|
|
663
|
+
* **`ArtifactDefinition`**: An internal representation of a registered artifact, holding its configuration (scope, lazy loading, retries, etc.) and runtime state (current instance, error, cleanup functions, dependency graph links).
|
|
664
|
+
* **`ArtifactFactory`**: A function that defines how an artifact instance is created. It receives an `ArtifactFactoryContext` which is crucial for declaring dependencies and managing its lifecycle.
|
|
665
|
+
* **`DataStore` (External Dependency)**: The container relies on an external data store (e.g., `@asaidimu/utils-store`) to manage global application state. The container interacts with it via `watch` (for reactive subscriptions) and `get` (for reading current state). The `buildPaths` function is used internally to derive specific state paths for granular dependency tracking.
|
|
666
|
+
* **`ArtifactWatcher`**: An observable interface for tracking the resolved state of an artifact over time, providing `get` for current value and `subscribe` for updates.
|
|
667
|
+
|
|
668
|
+
### Data Flow
|
|
669
|
+
|
|
670
|
+
1. **Registration (`container.register`)**: An `ArtifactFactory` is associated with a `key` and `ArtifactOptions` (scope, lazy, retries, etc.).
|
|
671
|
+
2. **Resolution (`container.resolve`)**:
|
|
672
|
+
* If the artifact is a `Singleton` and already built, the cached instance is returned.
|
|
673
|
+
* If not built, the `ArtifactFactory` is invoked within a specialized context.
|
|
674
|
+
* **Dependency Declaration (`ctx.use`)**: Inside the factory, `ctx.use` provides:
|
|
675
|
+
* `ctx.resolve(key)`: Declares a dependency on another artifact. This call recursively triggers resolution of the dependency.
|
|
676
|
+
* `ctx.select(selector)`: Declares a dependency on a slice of the global `DataStore` state. Internally, `buildPaths` extracts specific state paths from the selector.
|
|
677
|
+
3. **Dependency Graph Construction**: As dependencies are declared, the `ArtifactContainer` builds a precise, bidirectional dependency graph, linking artifacts to their dependents and subscribed state paths.
|
|
678
|
+
4. **Reactive Invalidation**:
|
|
679
|
+
* **State Changes**: When a `DataStore` value changes, its `watch` callback is invoked. If the changed path matches an `ArtifactDefinition`'s `stateDependencies`, `container.invalidate()` is called for that artifact.
|
|
680
|
+
* **Artifact Changes**: When an artifact is rebuilt or `yield()`s a new value, its `dependents` are identified and `container.invalidate()` is called for each.
|
|
681
|
+
5. **Rebuild Process**: `container.invalidate()` triggers:
|
|
682
|
+
* **Debouncing**: If configured, rebuilds are debounced to consolidate multiple rapid invalidations into a single rebuild.
|
|
683
|
+
* **Cleanup**: `onCleanup` hooks for the old instance are run.
|
|
684
|
+
* **New Instance**: The `ArtifactFactory` is re-executed, creating a new instance.
|
|
685
|
+
* **Graph Update**: The dependency graph is updated based on the new factory execution.
|
|
686
|
+
* **Notification**: `ArtifactWatcher` subscribers are notified of the new instance.
|
|
687
|
+
6. **Disposal (`container.dispose` / `container.unregister`)**: All resources (`onDispose` hooks, state subscriptions, instance cache) are released.
|
|
688
|
+
|
|
689
|
+
### Extension Points
|
|
690
|
+
|
|
691
|
+
The primary extension point is the **`ArtifactFactory` function**. By implementing custom factories, you can:
|
|
692
|
+
* Integrate with any third-party library or framework.
|
|
693
|
+
* Define complex business logic.
|
|
694
|
+
* Manage external resources like database connections, API clients, or message queues.
|
|
695
|
+
* Provide custom logging, monitoring, or telemetry by injecting those services as dependencies.
|
|
696
|
+
|
|
697
|
+
The `ArtifactContainer` itself is designed to be highly configurable via `ArtifactOptions`, allowing fine-tuning of scope, lazy loading, error handling, and reactive behavior.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## 👨💻 Development & Contributing
|
|
702
|
+
|
|
703
|
+
We welcome contributions! Please read the guidelines below.
|
|
704
|
+
|
|
705
|
+
### Development Setup
|
|
706
|
+
|
|
707
|
+
1. **Clone the repository**:
|
|
708
|
+
```bash
|
|
709
|
+
git clone https://github.com/asaidimu/utils.git
|
|
710
|
+
cd utils/src/artifacts
|
|
711
|
+
```
|
|
712
|
+
2. **Install dependencies**:
|
|
713
|
+
```bash
|
|
714
|
+
bun install
|
|
715
|
+
# or npm install
|
|
716
|
+
# or yarn install
|
|
717
|
+
```
|
|
718
|
+
3. **Build (if necessary for changes outside `src`)**:
|
|
719
|
+
```bash
|
|
720
|
+
bun run build
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Scripts
|
|
724
|
+
|
|
725
|
+
* `bun install`: Installs project dependencies.
|
|
726
|
+
* `bun run test`: Runs the test suite using Vitest.
|
|
727
|
+
* `bun run build`: Compiles the TypeScript source code into JavaScript.
|
|
728
|
+
|
|
729
|
+
### Testing
|
|
730
|
+
|
|
731
|
+
This project uses [Vitest](https://vitest.dev/) for testing.
|
|
732
|
+
|
|
733
|
+
* To run all tests:
|
|
734
|
+
```bash
|
|
735
|
+
bun run test
|
|
736
|
+
```
|
|
737
|
+
* To run tests in watch mode:
|
|
738
|
+
```bash
|
|
739
|
+
bun run test --watch
|
|
740
|
+
```
|
|
741
|
+
* Ensure all new features or bug fixes are accompanied by appropriate unit and/or integration tests.
|
|
742
|
+
|
|
743
|
+
### Contributing Guidelines
|
|
744
|
+
|
|
745
|
+
We appreciate your interest in contributing to `@asaidimu/utils-artifacts`!
|
|
746
|
+
Please refer to the main repository's [CONTRIBUTING.md](https://github.com/asaidimu/utils/blob/main/CONTRIBUTING.md) for detailed guidelines on:
|
|
747
|
+
|
|
748
|
+
* Reporting issues
|
|
749
|
+
* Submitting pull requests
|
|
750
|
+
* Coding standards
|
|
751
|
+
* Commit message conventions (we follow Conventional Commits)
|
|
752
|
+
|
|
753
|
+
### Issue Reporting
|
|
754
|
+
|
|
755
|
+
Found a bug or have a feature request? Please open an issue on our [GitHub Issues page](https://github.com/asaidimu/utils/issues).
|
|
756
|
+
Provide as much detail as possible, including steps to reproduce, expected behavior, and your environment.
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## ➕ Additional Information
|
|
761
|
+
|
|
762
|
+
### Troubleshooting
|
|
763
|
+
|
|
764
|
+
* **`ArtifactNotFoundError`**: This typically means you're trying to `resolve` an artifact that hasn't been `register`ed in the current container or any of its parent containers. Double-check your `key`s and registration calls.
|
|
765
|
+
* **`CircularDependencyError`**: Occurs when artifact A depends on B, and B depends back on A (directly or indirectly). This indicates a structural problem in your dependency graph. Redesign your artifacts to break the cycle. The error message will show the path of the cycle.
|
|
766
|
+
* **Artifact Not Updating Reactively**:
|
|
767
|
+
* Ensure your artifact is a `Singleton` (Transient artifacts don't cache or react to invalidations).
|
|
768
|
+
* Verify you are using `ctx.use()` and `ctx.select()` (for state) or `ctx.resolve()` (for other artifacts) inside your factory to declare dependencies.
|
|
769
|
+
* Check that your `DataStore` implementation is correctly triggering its `watch` callbacks.
|
|
770
|
+
* **`IllegalScopeError`**: You might be trying to `yield` a value from a `Transient` artifact. `yield` is only meaningful for `Singleton` artifacts, which maintain a persistent instance.
|
|
771
|
+
* **Timeout Errors**: Your artifact factory took longer than `timeoutMs` to complete. Consider increasing the `timeoutMs` or optimizing your factory's logic.
|
|
772
|
+
|
|
773
|
+
### FAQ
|
|
774
|
+
|
|
775
|
+
* **What is an "artifact" in this context?**
|
|
776
|
+
An artifact is simply any JavaScript/TypeScript value, object, or function that you want the `ArtifactContainer` to manage. This could be a service, a repository, a configuration object, a computed value, or even a simple primitive.
|
|
777
|
+
* **Why use this over a simpler DI library?**
|
|
778
|
+
`@asaidimu/utils-artifacts` goes beyond basic DI by adding a powerful reactive layer. It automatically handles the propagation of changes from global state or other artifacts, making it easier to build self-updating components and reactive systems without manual subscription management.
|
|
779
|
+
* **When should I use `Singleton` vs. `Transient`?**
|
|
780
|
+
* **Singleton**: For services, database connections, configuration objects, or any resource that should be shared and consistently available throughout your application. They are efficient as they are built once and reused.
|
|
781
|
+
* **Transient**: For objects that need a fresh instance every time they are requested, such as temporary calculators, request-scoped contexts, or factory functions that produce unique outputs.
|
|
782
|
+
* **How does `debounce` work?**
|
|
783
|
+
`debounce` prevents an artifact from rebuilding too frequently. If multiple invalidations occur within the specified `debounce` time, they are consolidated, and the artifact's factory is only run once after the activity ceases. This is crucial for performance with rapid state changes.
|
|
784
|
+
|
|
785
|
+
### Changelog / Roadmap
|
|
786
|
+
|
|
787
|
+
* **Changelog**: For a detailed list of changes, please refer to the [CHANGELOG.md](https://github.com/asaidimu/utils/blob/main/CHANGELOG.md) file in the main repository.
|
|
788
|
+
* **Roadmap**: Future plans and upcoming features are typically outlined in the main repository's [issue tracker](https://github.com/asaidimu/utils/issues) or project boards.
|
|
789
|
+
|
|
790
|
+
### License
|
|
791
|
+
|
|
792
|
+
This project is licensed under the MIT License. See the [LICENSE](https://github.com/asaidimu/utils/blob/main/LICENSE) file for details.
|
|
793
|
+
|
|
794
|
+
### Acknowledgments
|
|
795
|
+
|
|
796
|
+
* Developed as part of the `@asaidimu` utilities collection.
|
|
797
|
+
* Uses [Semantic Release](https://semantic-release.gitbook.io/semantic-release/) for automated versioning and publishing.
|
|
798
|
+
* Leverages [Vitest](https://vitest.dev/) for fast and reliable testing.
|
|
799
|
+
|