@tstdl/base 0.93.139 → 0.93.140
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 +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.js +2 -2
- package/audit/README.md +267 -0
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/lock/README.md +249 -0
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +1 -1
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/package.json +8 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/reflection/README.md +305 -0
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +300 -0
- package/task-queue/postgres/task-queue.d.ts +2 -1
- package/task-queue/postgres/task-queue.js +32 -2
- package/task-queue/task-context.js +1 -1
- package/task-queue/task-queue.d.ts +17 -0
- package/task-queue/task-queue.js +103 -45
- package/task-queue/tests/complex.test.js +4 -4
- package/task-queue/tests/dependencies.test.js +4 -2
- package/task-queue/tests/queue.test.js +111 -0
- package/task-queue/tests/worker.test.js +21 -13
- package/templates/README.md +287 -0
- package/testing/README.md +157 -0
- package/text/README.md +346 -0
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- package/utils/z-base32.js +1 -0
package/text/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# @tstdl/base/text
|
|
2
|
+
|
|
3
|
+
A powerful, reactive, and type-safe text localization module for TypeScript applications, built on signals. It provides seamless integration for internationalization (i18n) with automatic UI updates, parameter interpolation, and strong compile-time safety.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [✨ Features](#-features)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [LocalizationService](#localizationservice)
|
|
10
|
+
- [Localization Definitions](#localization-definitions)
|
|
11
|
+
- [DynamicText](#dynamictext)
|
|
12
|
+
- [Type-Safe Keys](#type-safe-keys)
|
|
13
|
+
- [🚀 Basic Usage](#-basic-usage)
|
|
14
|
+
- [1. Define Localization Structure](#1-define-localization-structure)
|
|
15
|
+
- [2. Create Localization Data](#2-create-localization-data)
|
|
16
|
+
- [3. Register and Use](#3-register-and-use)
|
|
17
|
+
- [🔧 Advanced Topics](#-advanced-topics)
|
|
18
|
+
- [Reactive Localization with Signals](#reactive-localization-with-signals)
|
|
19
|
+
- [Parameterized Text](#parameterized-text)
|
|
20
|
+
- [Localization Functions](#localization-functions)
|
|
21
|
+
- [Localizing Enums](#localizing-enums)
|
|
22
|
+
- [Resolving Nested Dynamic Text](#resolving-nested-dynamic-text)
|
|
23
|
+
- [Common Localizations](#common-localizations)
|
|
24
|
+
- [📚 API](#-api)
|
|
25
|
+
|
|
26
|
+
## ✨ Features
|
|
27
|
+
|
|
28
|
+
- **Type-Safe Keys**: Leverage TypeScript proxies to provide compile-time safety and autocompletion for localization keys, eliminating magic strings.
|
|
29
|
+
- **Reactive Architecture**: Built on Signals, allowing text to automatically update whenever the active language changes without manual subscription management.
|
|
30
|
+
- **Dynamic Text Support**: Uniformly handle static strings, Signals, and Observables as localizable content.
|
|
31
|
+
- **Parameter Interpolation**: Easily inject values into translation strings (e.g., `Hello {{name}}`).
|
|
32
|
+
- **Functional Localization**: Use functions for complex logic like pluralization or conditional formatting.
|
|
33
|
+
- **Enum Support**: First-class utilities for localizing TypeScript enums.
|
|
34
|
+
- **RxJS Compatibility**: Includes Observable-based alternatives for all reactive methods.
|
|
35
|
+
|
|
36
|
+
## Core Concepts
|
|
37
|
+
|
|
38
|
+
### LocalizationService
|
|
39
|
+
|
|
40
|
+
The `LocalizationService` is the singleton central hub. It manages the active language, stores translation data, and performs the actual text resolution. It exposes reactive signals for the current language and available languages.
|
|
41
|
+
|
|
42
|
+
### Localization Definitions
|
|
43
|
+
|
|
44
|
+
A `Localization` object defines the translations for a specific language. It consists of:
|
|
45
|
+
|
|
46
|
+
- **`language`**: Metadata like code (`en`) and name (`English`).
|
|
47
|
+
- **`keys`**: A nested object structure containing the translation strings or functions.
|
|
48
|
+
- **`enums`**: Definitions for translating TypeScript enums.
|
|
49
|
+
|
|
50
|
+
### DynamicText
|
|
51
|
+
|
|
52
|
+
`DynamicText` is a type alias for `ReactiveValue<LocalizableText>`. It represents a piece of text that might be:
|
|
53
|
+
|
|
54
|
+
1. A static string or localization key.
|
|
55
|
+
2. A `Signal` emitting localization keys.
|
|
56
|
+
3. An `Observable` emitting localization keys.
|
|
57
|
+
|
|
58
|
+
The module provides utilities to "resolve" a `DynamicText` into a `Signal<string>` that updates if either the source value changes _or_ the active language changes.
|
|
59
|
+
|
|
60
|
+
### Type-Safe Keys
|
|
61
|
+
|
|
62
|
+
Instead of using dot-notation strings (e.g., `'home.welcome'`), this module uses a proxy object generated by `getLocalizationKeys()`. This allows you to pass around references to keys (e.g., `keys.home.welcome`) that TypeScript understands, enabling refactoring support and compile-time checks.
|
|
63
|
+
|
|
64
|
+
## 🚀 Basic Usage
|
|
65
|
+
|
|
66
|
+
### 1. Define Localization Structure
|
|
67
|
+
|
|
68
|
+
Define the shape of your localization keys using TypeScript types. This ensures all languages implement the same keys.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { type Localization, type LocalizeItem, getLocalizationKeys } from '@tstdl/base/text';
|
|
72
|
+
|
|
73
|
+
// Define the structure
|
|
74
|
+
type AppLocalization = Localization<{
|
|
75
|
+
app: {
|
|
76
|
+
title: LocalizeItem;
|
|
77
|
+
greeting: LocalizeItem;
|
|
78
|
+
};
|
|
79
|
+
}>;
|
|
80
|
+
|
|
81
|
+
// Create the type-safe key proxy
|
|
82
|
+
export const keys = getLocalizationKeys<AppLocalization>();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Create Localization Data
|
|
86
|
+
|
|
87
|
+
Implement the localization for your supported languages.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { type AppLocalization } from './localization-types'; // from step 1
|
|
91
|
+
|
|
92
|
+
export const english: AppLocalization = {
|
|
93
|
+
language: { code: 'en', name: 'English' },
|
|
94
|
+
keys: {
|
|
95
|
+
app: {
|
|
96
|
+
title: 'My Awesome App',
|
|
97
|
+
greeting: 'Hello World',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
enums: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const german: AppLocalization = {
|
|
104
|
+
language: { code: 'de', name: 'Deutsch' },
|
|
105
|
+
keys: {
|
|
106
|
+
app: {
|
|
107
|
+
title: 'Meine Tolle App',
|
|
108
|
+
greeting: 'Hallo Welt',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
enums: [],
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. Register and Use
|
|
116
|
+
|
|
117
|
+
Inject the service, register the languages, and resolve text.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { inject } from '@tstdl/base/injector';
|
|
121
|
+
import { LocalizationService } from '@tstdl/base/text';
|
|
122
|
+
import { keys } from './localization-types';
|
|
123
|
+
import { english, german } from './localizations';
|
|
124
|
+
|
|
125
|
+
const localizationService = inject(LocalizationService);
|
|
126
|
+
|
|
127
|
+
// Register languages
|
|
128
|
+
localizationService.registerLocalization(english, german);
|
|
129
|
+
|
|
130
|
+
// Set active language
|
|
131
|
+
localizationService.setLanguage('en');
|
|
132
|
+
|
|
133
|
+
// Resolve text reactively (returns a Signal)
|
|
134
|
+
const titleSignal = localizationService.localize(keys.app.title);
|
|
135
|
+
|
|
136
|
+
console.log(titleSignal()); // "My Awesome App"
|
|
137
|
+
|
|
138
|
+
// Switch language
|
|
139
|
+
localizationService.setLanguage('de');
|
|
140
|
+
|
|
141
|
+
// Signal updates automatically
|
|
142
|
+
console.log(titleSignal()); // "Meine Tolle App"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Modular Localizations
|
|
146
|
+
|
|
147
|
+
`registerLocalization` automatically merges new keys and enums into existing language definitions if the language code matches. This allows different parts of your application or different modules to contribute translations to the same language incrementally.
|
|
148
|
+
|
|
149
|
+
### Handling Missing Keys and Parameters
|
|
150
|
+
|
|
151
|
+
- **Missing Key**: If a localization key is missing for the active language, the service returns the key wrapped in double underscores (e.g., `__app.title__`) and logs a warning.
|
|
152
|
+
- **Missing Parameter**: If a parameter is missing in the data object but required by the template string, it is replaced by the parameter name wrapped in double underscores (e.g., `Hello, __name__!`).
|
|
153
|
+
- **Missing Observable Value**: When resolving an `Observable` based `DynamicText` using `resolveDynamicText`, the initial value before the first emission is `[MISSING LOCALIZATION KEY]`.
|
|
154
|
+
|
|
155
|
+
## 🔧 Advanced Topics
|
|
156
|
+
|
|
157
|
+
### Reactive Localization with Signals
|
|
158
|
+
|
|
159
|
+
The `resolveDynamicText` function is the primary tool for handling text in reactive applications. It accepts static values, Signals, or Observables.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { signal } from '@tstdl/base/signals';
|
|
163
|
+
import { resolveDynamicText } from '@tstdl/base/text';
|
|
164
|
+
import { keys } from './localization-types';
|
|
165
|
+
|
|
166
|
+
// A signal that determines which key to show
|
|
167
|
+
const statusSignal = signal(keys.status.online);
|
|
168
|
+
|
|
169
|
+
// Result is a signal that updates if statusSignal changes OR language changes
|
|
170
|
+
const textSignal = resolveDynamicText(statusSignal);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Parameterized Text
|
|
174
|
+
|
|
175
|
+
You can define parameters in your strings using `{{ paramName }}` syntax. Use `localizationData` to bind values to these parameters.
|
|
176
|
+
|
|
177
|
+
**Definition:**
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
type AppLocalization = Localization<{
|
|
181
|
+
messages: {
|
|
182
|
+
welcome: LocalizeItem<{ name: string }>;
|
|
183
|
+
};
|
|
184
|
+
}>;
|
|
185
|
+
|
|
186
|
+
const en: AppLocalization = {
|
|
187
|
+
// ...
|
|
188
|
+
keys: {
|
|
189
|
+
messages: {
|
|
190
|
+
welcome: 'Welcome, {{name}}!',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
// ...
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Usage:**
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { localizationData, resolveDynamicText } from '@tstdl/base/text';
|
|
201
|
+
import { keys } from './localization-types';
|
|
202
|
+
|
|
203
|
+
// Create a data object binding the key and parameters
|
|
204
|
+
const data = localizationData(keys.messages.welcome, { name: 'Alice' });
|
|
205
|
+
|
|
206
|
+
const text = resolveDynamicText(data);
|
|
207
|
+
console.log(text()); // "Welcome, Alice!"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Localization Functions
|
|
211
|
+
|
|
212
|
+
For complex logic (like pluralization or conditional formatting), use a function instead of a string. Functions receive the parameters and a context object containing the `LocalizationService`.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const en: AppLocalization = {
|
|
216
|
+
// ...
|
|
217
|
+
keys: {
|
|
218
|
+
items: {
|
|
219
|
+
count: ({ count }, { localizationService }) => (count === 1 ? '1 Item' : `${count} Items`),
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
// ...
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Usage
|
|
226
|
+
const text = resolveDynamicText(localizationData(keys.items.count, { count: 5 }));
|
|
227
|
+
console.log(text()); // "5 Items"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Localizing Enums
|
|
231
|
+
|
|
232
|
+
The module provides specific helpers for `enum` types to ensure type safety and ease of use.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { defineEnum, type EnumType } from '@tstdl/base/enumeration';
|
|
236
|
+
import { enumerationLocalization, localizeEnum } from '@tstdl/base/text';
|
|
237
|
+
|
|
238
|
+
// 1. Define Enum
|
|
239
|
+
const Status = defineEnum('Status', {
|
|
240
|
+
Active: 'active',
|
|
241
|
+
Inactive: 'inactive',
|
|
242
|
+
});
|
|
243
|
+
type Status = EnumType<typeof Status>;
|
|
244
|
+
|
|
245
|
+
// 2. Add to Localization Type
|
|
246
|
+
type AppLocalization = Localization<
|
|
247
|
+
{
|
|
248
|
+
/* ... keys ... */
|
|
249
|
+
},
|
|
250
|
+
[typeof Status] // Add enum type here
|
|
251
|
+
>;
|
|
252
|
+
|
|
253
|
+
// 3. Define Translations
|
|
254
|
+
const en: AppLocalization = {
|
|
255
|
+
// ...
|
|
256
|
+
enums: [
|
|
257
|
+
enumerationLocalization(Status, 'Status', {
|
|
258
|
+
[Status.Active]: 'Active User',
|
|
259
|
+
[Status.Inactive]: 'Inactive User',
|
|
260
|
+
}),
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// 4. Usage
|
|
265
|
+
const statusText = localizationService.localizeEnum(Status, Status.Active);
|
|
266
|
+
console.log(statusText()); // "Active User"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Resolving Nested Dynamic Text
|
|
270
|
+
|
|
271
|
+
When working with lists of objects where one property is a `DynamicText` (e.g., a navigation menu or a list of options), `resolveNestedDynamicTexts` helps transform the entire array efficiently.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { resolveNestedDynamicTexts, type DynamicText } from '@tstdl/base/text';
|
|
275
|
+
|
|
276
|
+
type MenuItem = {
|
|
277
|
+
id: string;
|
|
278
|
+
label: DynamicText; // Can be a string, key, or localizationData
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const menuItems: MenuItem[] = [
|
|
282
|
+
{ id: 'home', label: keys.nav.home },
|
|
283
|
+
{ id: 'settings', label: keys.nav.settings },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
// Returns a Signal<Array<{ id: string, label: string }>>
|
|
287
|
+
// The 'label' property is now the resolved string
|
|
288
|
+
const resolvedMenu = resolveNestedDynamicTexts(menuItems, 'label');
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Common Localizations
|
|
292
|
+
|
|
293
|
+
The module includes a set of common localizations (Yes, No, Ok, Cancel, etc.) that you can merge into your application to avoid repetition.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { englishTstdlCommonLocalization, germanTstdlCommonLocalization } from '@tstdl/base/text';
|
|
297
|
+
|
|
298
|
+
localizationService.registerLocalization(englishTstdlCommonLocalization, germanTstdlCommonLocalization);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## 📚 API
|
|
302
|
+
|
|
303
|
+
### LocalizationService
|
|
304
|
+
|
|
305
|
+
| Method | Description |
|
|
306
|
+
| :--------------------------------------- | :-------------------------------------------------------------------------- |
|
|
307
|
+
| `registerLocalization(...localizations)` | Registers one or more localization definitions. Merges if language exists. |
|
|
308
|
+
| `setLanguage(code)` | Sets the active language by code or `Language` object. |
|
|
309
|
+
| `getLanguage(code)` | Retrieves a registered `Language` by its code. |
|
|
310
|
+
| `hasLanguage(code)` | Checks if a language is registered. |
|
|
311
|
+
| `localize(data)` | Returns a `Signal<string>` for the given key/data. |
|
|
312
|
+
| `localize$(data)` | Returns an `Observable<string>` for the given key/data. |
|
|
313
|
+
| `localizeOnce(data)` | Returns a `string` (non-reactive) for the current language. |
|
|
314
|
+
| `localizeEnum(enum, value)` | Returns a `Signal<string>` for the enum value. |
|
|
315
|
+
| `localizeEnum$(enum, value)` | Returns an `Observable<string>` for the enum value. |
|
|
316
|
+
| `localizeEnumOnce(enum, value)` | Returns a `string` (non-reactive) for the enum value. |
|
|
317
|
+
| `activeLanguage` | Read-only signal of the currently active `Language`. |
|
|
318
|
+
| `availableLanguages` | Read-only signal of all registered `Language`s. |
|
|
319
|
+
|
|
320
|
+
### Utilities
|
|
321
|
+
|
|
322
|
+
| Function | Description |
|
|
323
|
+
| :------------------------------------ | :-------------------------------------------------------------------------- |
|
|
324
|
+
| `getLocalizationKeys<T>()` | Returns a proxy object for type-safe access to localization keys. |
|
|
325
|
+
| `localizationData(key, params)` | Creates a typed object containing a key and its parameters. |
|
|
326
|
+
| `resolveDynamicText(text)` | Converts a `DynamicText` into a `Signal<string>`. |
|
|
327
|
+
| `resolveDynamicText$(text)` | Converts a `DynamicText` into an `Observable<string>`. |
|
|
328
|
+
| `resolveDynamicTexts(texts)` | Converts an array of `DynamicText` into a `Signal<string[]>`. |
|
|
329
|
+
| `resolveDynamicTexts$(texts)` | Converts an array of `DynamicText` into an `Observable<string[]>`. |
|
|
330
|
+
| `resolveNestedDynamicText(obj, key)` | Resolves a `DynamicText` property within an object (Signal). |
|
|
331
|
+
| `resolveNestedDynamicText$(obj, key)` | Resolves a `DynamicText` property within an object (Observable). |
|
|
332
|
+
| `resolveNestedDynamicTexts(arr, key)` | Resolves a `DynamicText` property within an array of objects (Signal). |
|
|
333
|
+
| `resolveNestedDynamicTexts$(arr, key)`| Resolves a `DynamicText` property within an array of objects (Observable). |
|
|
334
|
+
| `enumerationLocalization(...)` | Helper to define enum translations in a `Localization` object. |
|
|
335
|
+
| `autoEnumerationLocalization(enum)` | Generates default translations using enum key names as values. |
|
|
336
|
+
|
|
337
|
+
### Types
|
|
338
|
+
|
|
339
|
+
| Type | Description |
|
|
340
|
+
| :--------------------- | :--------------------------------------------------------------------------------- |
|
|
341
|
+
| `Localization` | The structure for defining translations (language, keys, enums). |
|
|
342
|
+
| `LocalizeItem<Params>` | A translation value: either a string or a `LocalizeFunction`. |
|
|
343
|
+
| `LocalizeFunction<P>` | A function returning a string, receiving parameters and `LocalizeFunctionContext`. |
|
|
344
|
+
| `DynamicText` | `ReactiveValue<LocalizableText>`. The standard input type for localizable content. |
|
|
345
|
+
| `LocalizableText` | `string | LocalizationData`. |
|
|
346
|
+
| `LocalizationData` | Key, `LocalizationDataObject`, or `EnumLocalizationKey`. |
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# @tstdl/base/threading
|
|
2
|
+
|
|
3
|
+
The `threading` module provides a unified, type-safe abstraction for multi-threading in both Node.js and browser environments. It utilizes a managed thread pool and an RPC mechanism to easily offload CPU-intensive tasks to worker threads without manually managing message passing.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [✨ Features](#-features)
|
|
8
|
+
2. [Core Concepts](#core-concepts)
|
|
9
|
+
3. [🚀 Basic Usage](#-basic-usage)
|
|
10
|
+
4. [🔧 Advanced Topics](#-advanced-topics)
|
|
11
|
+
- [Named Workers (Multiple Functions)](#named-workers-multiple-functions)
|
|
12
|
+
- [Configuration](#configuration)
|
|
13
|
+
5. [📚 API](#-api)
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- **Cross-Platform:** Works seamlessly in Node.js (using `worker_threads`) and Browsers (using `Web Workers`).
|
|
18
|
+
- **Thread Pooling:** Automatically manages a pool of workers to reuse resources and limit concurrency.
|
|
19
|
+
- **RPC Communication:** Call worker functions directly as if they were local asynchronous functions, handling argument and return value serialization automatically.
|
|
20
|
+
- **Type Safety:** leveraging TypeScript to ensure the main thread invokes worker functions with correct arguments and return types.
|
|
21
|
+
- **Resource Management:** Implements `AsyncDisposable` for clean teardown of thread pools.
|
|
22
|
+
|
|
23
|
+
## Core Concepts
|
|
24
|
+
|
|
25
|
+
### Thread Pool
|
|
26
|
+
|
|
27
|
+
The `ThreadPool` is the main orchestrator running on the main thread. It spawns a specified number of worker threads based on a provided script URL. It manages the lifecycle of these workers and distributes tasks to them.
|
|
28
|
+
|
|
29
|
+
### RPC (Remote Procedure Call)
|
|
30
|
+
|
|
31
|
+
Instead of manually posting messages (`postMessage`) and listening for events, this module uses an RPC layer. You "expose" a function inside the worker, and the `ThreadPool` creates a "remote" proxy for that function. When you call the proxy, arguments are sent to the worker, the function executes, and the result is sent back.
|
|
32
|
+
|
|
33
|
+
### Worker Script
|
|
34
|
+
|
|
35
|
+
This is the separate entry point file that runs inside the thread. It imports `exposeThreadWorker` to make specific functions available to the pool.
|
|
36
|
+
|
|
37
|
+
## 🚀 Basic Usage
|
|
38
|
+
|
|
39
|
+
To use the threading module, you need two parts: the worker script (the code running in the thread) and the main application code (which creates the pool).
|
|
40
|
+
|
|
41
|
+
### 1. Create the Worker (`worker.ts`)
|
|
42
|
+
|
|
43
|
+
Define your CPU-intensive function, export its type (for type safety in the main thread), and expose it.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { exposeThreadWorker } from '@tstdl/base/threading';
|
|
47
|
+
|
|
48
|
+
// 1. Define the function to run in the thread
|
|
49
|
+
function heavyComputation(input: number): number {
|
|
50
|
+
// Simulate heavy work
|
|
51
|
+
const start = Date.now();
|
|
52
|
+
while (Date.now() - start < 500);
|
|
53
|
+
return input * 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Export the type so the main thread knows the signature
|
|
57
|
+
export type HeavyComputation = typeof heavyComputation;
|
|
58
|
+
|
|
59
|
+
// 3. Expose the function as the default handler
|
|
60
|
+
exposeThreadWorker(heavyComputation);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Use the Pool (`main.ts`)
|
|
64
|
+
|
|
65
|
+
Instantiate the `ThreadPool` pointing to the compiled worker script.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { ThreadPool } from '@tstdl/base/threading';
|
|
69
|
+
import { Logger } from '@tstdl/base/logger';
|
|
70
|
+
import type { HeavyComputation } from './worker.js'; // Import type only
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const logger = new Logger('App');
|
|
74
|
+
|
|
75
|
+
// Point to the compiled JS file of the worker
|
|
76
|
+
const workerUrl = new URL('./worker.js', import.meta.url);
|
|
77
|
+
|
|
78
|
+
// Create the pool
|
|
79
|
+
const pool = new ThreadPool(workerUrl, logger);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Get a type-safe processor function
|
|
83
|
+
// The generic argument <HeavyComputation> ensures type safety
|
|
84
|
+
const process = pool.getProcessor<HeavyComputation>();
|
|
85
|
+
|
|
86
|
+
// Run the task
|
|
87
|
+
const result = await process(21);
|
|
88
|
+
console.log(`Result: ${result}`); // Result: 42
|
|
89
|
+
} finally {
|
|
90
|
+
// Clean up threads
|
|
91
|
+
await pool.dispose();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 🔧 Advanced Topics
|
|
99
|
+
|
|
100
|
+
### Named Workers (Multiple Functions)
|
|
101
|
+
|
|
102
|
+
You can expose multiple functions from a single worker script by giving them unique names.
|
|
103
|
+
|
|
104
|
+
**Worker (`multi-worker.ts`):**
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { exposeThreadWorker } from '@tstdl/base/threading';
|
|
108
|
+
|
|
109
|
+
function add(a: number, b: number): number {
|
|
110
|
+
return a + b;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function subtract(a: number, b: number): number {
|
|
114
|
+
return a - b;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type Add = typeof add;
|
|
118
|
+
export type Subtract = typeof subtract;
|
|
119
|
+
|
|
120
|
+
// Expose with specific names
|
|
121
|
+
exposeThreadWorker(add, 'add');
|
|
122
|
+
exposeThreadWorker(subtract, 'subtract');
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Main (`main.ts`):**
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { ThreadPool } from '@tstdl/base/threading';
|
|
129
|
+
import { Logger } from '@tstdl/base/logger';
|
|
130
|
+
import type { Add, Subtract } from './multi-worker.js';
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
const logger = new Logger('App');
|
|
134
|
+
const pool = new ThreadPool(new URL('./multi-worker.js', import.meta.url), logger);
|
|
135
|
+
|
|
136
|
+
// Get processors by name
|
|
137
|
+
const addRemote = pool.getProcessor<Add>('add');
|
|
138
|
+
const subtractRemote = pool.getProcessor<Subtract>('subtract');
|
|
139
|
+
|
|
140
|
+
const sum = await addRemote(5, 3);
|
|
141
|
+
const diff = await subtractRemote(10, 4);
|
|
142
|
+
|
|
143
|
+
console.log({ sum, diff }); // { sum: 8, diff: 6 }
|
|
144
|
+
|
|
145
|
+
await pool.dispose();
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Best Practices
|
|
150
|
+
|
|
151
|
+
- **Offload CPU-Intensive Work Only:** Threading has overhead due to serialization and communication. Use it for heavy computations (e.g., image processing, complex math, large data parsing) rather than simple asynchronous I/O.
|
|
152
|
+
- **Minimize Data Transfer:** Arguments and return values are serialized (using the Structured Clone algorithm). Large objects can slow down communication.
|
|
153
|
+
- **Clean Up Resources:** Always call `dispose()` or use `[Symbol.asyncDispose]` (e.g., via `await using`) to ensure worker threads are terminated when they are no longer needed.
|
|
154
|
+
- **Worker Script Paths:** In Node.js, ensure the worker URL points to the _compiled_ `.js` file, especially when using TypeScript.
|
|
155
|
+
|
|
156
|
+
### Configuration
|
|
157
|
+
|
|
158
|
+
You can configure the underlying worker options and the number of threads in the pool.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { ThreadPool } from '@tstdl/base/threading';
|
|
162
|
+
import { Logger } from '@tstdl/base/logger';
|
|
163
|
+
|
|
164
|
+
const pool = new ThreadPool(new URL('./worker.js', import.meta.url), new Logger('Pool'), {
|
|
165
|
+
// Number of workers to spawn.
|
|
166
|
+
// Defaults to half of CPU cores (via hardwareConcurrency / 2) or 4.
|
|
167
|
+
threadCount: 4,
|
|
168
|
+
|
|
169
|
+
// Node.js specific options (passed to node:worker_threads Worker constructor)
|
|
170
|
+
workerData: { some: 'data' },
|
|
171
|
+
|
|
172
|
+
// Browser specific options (passed to Web Worker constructor)
|
|
173
|
+
type: 'module',
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 📚 API
|
|
178
|
+
|
|
179
|
+
### Classes
|
|
180
|
+
|
|
181
|
+
| Class | Description |
|
|
182
|
+
| :----------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| `ThreadPool` | Manages a pool of worker threads. Implements `AsyncDisposable` for clean resource cleanup. Provides type-safe access to worker functions. |
|
|
184
|
+
|
|
185
|
+
#### `ThreadPool`
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
class ThreadPool implements AsyncDisposable {
|
|
189
|
+
/** The URL or path to the worker script. */
|
|
190
|
+
readonly url: string | URL;
|
|
191
|
+
|
|
192
|
+
/** Configuration options used for this pool. */
|
|
193
|
+
readonly options: ThreadOptions | undefined;
|
|
194
|
+
|
|
195
|
+
constructor(url: string | URL, logger: Logger, options?: ThreadOptions);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns a type-safe function that, when called, executes the task on an available worker.
|
|
199
|
+
* This is the preferred way to interact with the pool.
|
|
200
|
+
* @param name The name of the exposed function in the worker (default: 'default').
|
|
201
|
+
*/
|
|
202
|
+
getProcessor<T extends ThreadWorker>(name?: string): (...args: Parameters<T>) => Promise<ReturnType<T>>;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Directly executes a task on an available worker.
|
|
206
|
+
* @param name The name of the exposed function in the worker.
|
|
207
|
+
* @param args Arguments to pass to the worker function.
|
|
208
|
+
*/
|
|
209
|
+
process<T extends ThreadWorker>(name: string, ...args: Parameters<T>): Promise<ReturnType<T>>;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Terminates all workers in the pool and releases resources.
|
|
213
|
+
*/
|
|
214
|
+
dispose(): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Functions
|
|
219
|
+
|
|
220
|
+
| Function | Description |
|
|
221
|
+
| :------------------- | :--------------------------------------------------------------------------------- |
|
|
222
|
+
| `exposeThreadWorker` | Exposes a function inside a worker script so it can be called by the `ThreadPool`. |
|
|
223
|
+
|
|
224
|
+
#### `exposeThreadWorker`
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
function exposeThreadWorker<T extends ThreadWorker>(worker: T, name?: string): void;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `worker`: The function (synchronous or asynchronous) to expose.
|
|
231
|
+
- `name`: An optional name for the function. Defaults to `'default'`.
|
|
232
|
+
|
|
233
|
+
### Types
|
|
234
|
+
|
|
235
|
+
| Type | Definition | Description |
|
|
236
|
+
| :-------------- | :--------------------------------------------------------------------------------- | :---------------------------------------------------------- |
|
|
237
|
+
| `ThreadWorker` | `(...args: any[]) => any \| Promise<any>` | Represents the signature of a function running in a thread. |
|
|
238
|
+
| `ThreadOptions` | `(WorkerOptions \| NodeWorkerThreads.WorkerOptions) & { threadCount?: number }` | Configuration options for the `ThreadPool`. |
|