@venizia/ignis-helpers 0.1.1-2 → 0.1.1-4
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 +150 -2931
- package/dist/common/redact.d.ts +2 -17
- package/dist/common/redact.d.ts.map +1 -1
- package/dist/common/redact.js +21 -27
- package/dist/common/redact.js.map +1 -1
- package/dist/modules/base.d.ts +3 -3
- package/dist/modules/base.d.ts.map +1 -1
- package/dist/modules/base.js.map +1 -1
- package/dist/modules/cron/cron.helper.d.ts +1 -5
- package/dist/modules/cron/cron.helper.d.ts.map +1 -1
- package/dist/modules/cron/cron.helper.js +1 -5
- package/dist/modules/cron/cron.helper.js.map +1 -1
- package/dist/modules/env/app-env.d.ts +1 -5
- package/dist/modules/env/app-env.d.ts.map +1 -1
- package/dist/modules/env/app-env.js +1 -5
- package/dist/modules/env/app-env.js.map +1 -1
- package/dist/modules/error/index.d.ts +5 -17
- package/dist/modules/error/index.d.ts.map +1 -1
- package/dist/modules/error/index.js +5 -16
- package/dist/modules/error/index.js.map +1 -1
- package/dist/modules/error/types.d.ts +3 -7
- package/dist/modules/error/types.d.ts.map +1 -1
- package/dist/modules/error/types.js +3 -7
- package/dist/modules/error/types.js.map +1 -1
- package/dist/modules/logger/base/abstract.d.ts +13 -0
- package/dist/modules/logger/base/abstract.d.ts.map +1 -0
- package/dist/modules/logger/base/abstract.js +8 -0
- package/dist/modules/logger/base/abstract.js.map +1 -0
- package/dist/modules/logger/base/base.d.ts +30 -0
- package/dist/modules/logger/base/base.d.ts.map +1 -0
- package/dist/modules/logger/base/base.js +44 -0
- package/dist/modules/logger/base/base.js.map +1 -0
- package/dist/modules/logger/base/index.d.ts +3 -0
- package/dist/modules/logger/base/index.d.ts.map +1 -0
- package/dist/modules/logger/base/index.js +19 -0
- package/dist/modules/logger/base/index.js.map +1 -0
- package/dist/modules/logger/common/constants.d.ts +32 -0
- package/dist/modules/logger/common/constants.d.ts.map +1 -0
- package/dist/modules/logger/common/constants.js +57 -0
- package/dist/modules/logger/common/constants.js.map +1 -0
- package/dist/modules/logger/common/index.d.ts +3 -0
- package/dist/modules/logger/common/index.d.ts.map +1 -0
- package/dist/modules/logger/common/index.js +19 -0
- package/dist/modules/logger/common/index.js.map +1 -0
- package/dist/modules/logger/common/types.d.ts +21 -0
- package/dist/modules/logger/common/types.d.ts.map +1 -0
- package/dist/modules/logger/common/types.js +3 -0
- package/dist/modules/logger/common/types.js.map +1 -0
- package/dist/modules/logger/factory.d.ts +15 -2
- package/dist/modules/logger/factory.d.ts.map +1 -1
- package/dist/modules/logger/factory.js +87 -3
- package/dist/modules/logger/factory.js.map +1 -1
- package/dist/modules/logger/formatting/deep-splat.d.ts +12 -0
- package/dist/modules/logger/formatting/deep-splat.d.ts.map +1 -0
- package/dist/modules/logger/formatting/deep-splat.js +59 -0
- package/dist/modules/logger/formatting/deep-splat.js.map +1 -0
- package/dist/modules/logger/formatting/index.d.ts.map +1 -0
- package/dist/modules/logger/formatting/index.js.map +1 -0
- package/dist/modules/logger/hf/common/constants.d.ts +23 -0
- package/dist/modules/logger/hf/common/constants.d.ts.map +1 -0
- package/dist/modules/logger/hf/common/constants.js +31 -0
- package/dist/modules/logger/hf/common/constants.js.map +1 -0
- package/dist/modules/logger/hf/common/index.d.ts +3 -0
- package/dist/modules/logger/hf/common/index.d.ts.map +1 -0
- package/dist/modules/logger/hf/common/index.js +19 -0
- package/dist/modules/logger/hf/common/index.js.map +1 -0
- package/dist/modules/logger/hf/common/types.d.ts +25 -0
- package/dist/modules/logger/hf/common/types.d.ts.map +1 -0
- package/dist/modules/logger/hf/common/types.js +3 -0
- package/dist/modules/logger/hf/common/types.js.map +1 -0
- package/dist/modules/logger/hf/flusher.d.ts +15 -0
- package/dist/modules/logger/hf/flusher.d.ts.map +1 -0
- package/dist/modules/logger/hf/flusher.js +111 -0
- package/dist/modules/logger/hf/flusher.js.map +1 -0
- package/dist/modules/logger/hf/index.d.ts +5 -0
- package/dist/modules/logger/hf/index.d.ts.map +1 -0
- package/dist/modules/logger/hf/index.js +23 -0
- package/dist/modules/logger/hf/index.js.map +1 -0
- package/dist/modules/logger/hf/logger.d.ts +30 -0
- package/dist/modules/logger/hf/logger.d.ts.map +1 -0
- package/dist/modules/logger/hf/logger.js +110 -0
- package/dist/modules/logger/hf/logger.js.map +1 -0
- package/dist/modules/logger/hf/ring.d.ts +3 -0
- package/dist/modules/logger/hf/ring.d.ts.map +1 -0
- package/dist/modules/logger/hf/ring.js +18 -0
- package/dist/modules/logger/hf/ring.js.map +1 -0
- package/dist/modules/logger/index.d.ts +4 -6
- package/dist/modules/logger/index.d.ts.map +1 -1
- package/dist/modules/logger/index.js +4 -6
- package/dist/modules/logger/index.js.map +1 -1
- package/dist/modules/logger/pino/common/index.d.ts +2 -0
- package/dist/modules/logger/pino/common/index.d.ts.map +1 -0
- package/dist/modules/logger/pino/common/index.js +18 -0
- package/dist/modules/logger/pino/common/index.js.map +1 -0
- package/dist/modules/logger/pino/common/types.d.ts +13 -0
- package/dist/modules/logger/pino/common/types.d.ts.map +1 -0
- package/dist/modules/logger/pino/common/types.js +3 -0
- package/dist/modules/logger/pino/common/types.js.map +1 -0
- package/dist/modules/logger/pino/define.d.ts +73 -0
- package/dist/modules/logger/pino/define.d.ts.map +1 -0
- package/dist/modules/logger/pino/define.js +185 -0
- package/dist/modules/logger/pino/define.js.map +1 -0
- package/dist/modules/logger/pino/index.d.ts +8 -0
- package/dist/modules/logger/pino/index.d.ts.map +1 -0
- package/dist/modules/logger/pino/index.js +24 -0
- package/dist/modules/logger/pino/index.js.map +1 -0
- package/dist/modules/logger/pino/logger.d.ts +26 -0
- package/dist/modules/logger/pino/logger.d.ts.map +1 -0
- package/dist/modules/logger/pino/logger.js +41 -0
- package/dist/modules/logger/pino/logger.js.map +1 -0
- package/dist/modules/logger/winston/common/index.d.ts +2 -0
- package/dist/modules/logger/winston/common/index.d.ts.map +1 -0
- package/dist/modules/logger/winston/common/index.js +18 -0
- package/dist/modules/logger/winston/common/index.js.map +1 -0
- package/dist/modules/logger/winston/common/types.d.ts +47 -0
- package/dist/modules/logger/winston/common/types.d.ts.map +1 -0
- package/dist/modules/logger/winston/common/types.js +3 -0
- package/dist/modules/logger/winston/common/types.js.map +1 -0
- package/dist/modules/logger/winston/define.d.ts +25 -0
- package/dist/modules/logger/winston/define.d.ts.map +1 -0
- package/dist/modules/logger/{default-logger.js → winston/define.js} +87 -49
- package/dist/modules/logger/winston/define.js.map +1 -0
- package/dist/modules/logger/winston/formatters/deep-splat.d.ts +7 -0
- package/dist/modules/logger/winston/formatters/deep-splat.d.ts.map +1 -0
- package/dist/modules/logger/winston/formatters/deep-splat.js +23 -0
- package/dist/modules/logger/winston/formatters/deep-splat.js.map +1 -0
- package/dist/modules/logger/winston/formatters/index.d.ts +2 -0
- package/dist/modules/logger/winston/formatters/index.d.ts.map +1 -0
- package/dist/modules/logger/winston/formatters/index.js +18 -0
- package/dist/modules/logger/winston/formatters/index.js.map +1 -0
- package/dist/modules/logger/winston/index.d.ts +6 -0
- package/dist/modules/logger/winston/index.d.ts.map +1 -0
- package/dist/modules/logger/winston/index.js +29 -0
- package/dist/modules/logger/winston/index.js.map +1 -0
- package/dist/modules/logger/winston/logger.d.ts +24 -0
- package/dist/modules/logger/winston/logger.d.ts.map +1 -0
- package/dist/modules/logger/winston/logger.js +39 -0
- package/dist/modules/logger/winston/logger.js.map +1 -0
- package/dist/modules/logger/{transports → winston/transports}/dgram.transport.d.ts +1 -8
- package/dist/modules/logger/winston/transports/dgram.transport.d.ts.map +1 -0
- package/dist/modules/logger/{transports → winston/transports}/dgram.transport.js +6 -1
- package/dist/modules/logger/winston/transports/dgram.transport.js.map +1 -0
- package/dist/modules/logger/winston/transports/index.d.ts.map +1 -0
- package/dist/modules/logger/winston/transports/index.js.map +1 -0
- package/dist/modules/network/tcp-socket/base-tcp-client.helper.d.ts +1 -5
- package/dist/modules/network/tcp-socket/base-tcp-client.helper.d.ts.map +1 -1
- package/dist/modules/network/tcp-socket/base-tcp-client.helper.js +1 -5
- package/dist/modules/network/tcp-socket/base-tcp-client.helper.js.map +1 -1
- package/dist/modules/network/tcp-socket/base-tcp-server.helper.d.ts +1 -9
- package/dist/modules/network/tcp-socket/base-tcp-server.helper.d.ts.map +1 -1
- package/dist/modules/network/tcp-socket/base-tcp-server.helper.js +4 -13
- package/dist/modules/network/tcp-socket/base-tcp-server.helper.js.map +1 -1
- package/dist/modules/pool/abstract.d.ts +5 -22
- package/dist/modules/pool/abstract.d.ts.map +1 -1
- package/dist/modules/pool/abstract.js +5 -22
- package/dist/modules/pool/abstract.js.map +1 -1
- package/dist/modules/pool/base.d.ts +1 -3
- package/dist/modules/pool/base.d.ts.map +1 -1
- package/dist/modules/pool/base.js +1 -3
- package/dist/modules/pool/base.js.map +1 -1
- package/dist/modules/pool/types.d.ts +2 -3
- package/dist/modules/pool/types.d.ts.map +1 -1
- package/dist/modules/queue/common/hook.d.ts +7 -12
- package/dist/modules/queue/common/hook.d.ts.map +1 -1
- package/dist/modules/queue/common/hook.js +4 -9
- package/dist/modules/queue/common/hook.js.map +1 -1
- package/dist/modules/queue/internal/hf/helper.d.ts +3 -7
- package/dist/modules/queue/internal/hf/helper.d.ts.map +1 -1
- package/dist/modules/queue/internal/hf/helper.js +3 -7
- package/dist/modules/queue/internal/hf/helper.js.map +1 -1
- package/dist/modules/queue/kafka/admin.d.ts +1 -17
- package/dist/modules/queue/kafka/admin.d.ts.map +1 -1
- package/dist/modules/queue/kafka/admin.js +1 -17
- package/dist/modules/queue/kafka/admin.js.map +1 -1
- package/dist/modules/queue/kafka/base.d.ts +2 -6
- package/dist/modules/queue/kafka/base.d.ts.map +1 -1
- package/dist/modules/queue/kafka/base.js +2 -6
- package/dist/modules/queue/kafka/base.js.map +1 -1
- package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.d.ts +3 -7
- package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.d.ts.map +1 -1
- package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.js +3 -7
- package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.js.map +1 -1
- package/dist/modules/queue/kafka/consumer.d.ts +1 -26
- package/dist/modules/queue/kafka/consumer.d.ts.map +1 -1
- package/dist/modules/queue/kafka/consumer.js +1 -26
- package/dist/modules/queue/kafka/consumer.js.map +1 -1
- package/dist/modules/queue/kafka/producer.d.ts +3 -23
- package/dist/modules/queue/kafka/producer.d.ts.map +1 -1
- package/dist/modules/queue/kafka/producer.js +3 -23
- package/dist/modules/queue/kafka/producer.js.map +1 -1
- package/dist/modules/queue/kafka/schema/registry.d.ts +2 -14
- package/dist/modules/queue/kafka/schema/registry.d.ts.map +1 -1
- package/dist/modules/queue/kafka/schema/registry.js +2 -14
- package/dist/modules/queue/kafka/schema/registry.js.map +1 -1
- package/dist/modules/redis/utility.d.ts +4 -10
- package/dist/modules/redis/utility.d.ts.map +1 -1
- package/dist/modules/redis/utility.js +3 -13
- package/dist/modules/redis/utility.js.map +1 -1
- package/dist/modules/secrets/base/abstract.helper.d.ts.map +1 -1
- package/dist/modules/secrets/base/abstract.helper.js +3 -4
- package/dist/modules/secrets/base/abstract.helper.js.map +1 -1
- package/dist/modules/secrets/hashicorp/hashicorp.helper.d.ts +1 -5
- package/dist/modules/secrets/hashicorp/hashicorp.helper.d.ts.map +1 -1
- package/dist/modules/secrets/hashicorp/hashicorp.helper.js +1 -5
- package/dist/modules/secrets/hashicorp/hashicorp.helper.js.map +1 -1
- package/dist/modules/secrets/index.js +1 -1
- package/dist/modules/secrets/index.js.map +1 -1
- package/dist/modules/socket/socket-io/server/helper.d.ts +1 -5
- package/dist/modules/socket/socket-io/server/helper.d.ts.map +1 -1
- package/dist/modules/socket/socket-io/server/helper.js +1 -5
- package/dist/modules/socket/socket-io/server/helper.js.map +1 -1
- package/dist/modules/socket/websocket/server/helper.d.ts +2 -10
- package/dist/modules/socket/websocket/server/helper.d.ts.map +1 -1
- package/dist/modules/socket/websocket/server/helper.js +2 -10
- package/dist/modules/socket/websocket/server/helper.js.map +1 -1
- package/dist/modules/storage/base.d.ts.map +1 -1
- package/dist/modules/storage/base.js +3 -7
- package/dist/modules/storage/base.js.map +1 -1
- package/dist/modules/storage/bun-s3/utility.d.ts.map +1 -1
- package/dist/modules/storage/bun-s3/utility.js +1 -4
- package/dist/modules/storage/bun-s3/utility.js.map +1 -1
- package/dist/modules/uid/helper.d.ts +1 -6
- package/dist/modules/uid/helper.d.ts.map +1 -1
- package/dist/modules/uid/helper.js +1 -6
- package/dist/modules/uid/helper.js.map +1 -1
- package/dist/modules/worker-thread/base.d.ts +1 -5
- package/dist/modules/worker-thread/base.d.ts.map +1 -1
- package/dist/modules/worker-thread/base.js +1 -5
- package/dist/modules/worker-thread/base.js.map +1 -1
- package/dist/modules/worker-thread/worker-bus.d.ts +1 -5
- package/dist/modules/worker-thread/worker-bus.d.ts.map +1 -1
- package/dist/modules/worker-thread/worker-bus.js +1 -5
- package/dist/modules/worker-thread/worker-bus.js.map +1 -1
- package/dist/utilities/module.utility.d.ts +7 -10
- package/dist/utilities/module.utility.d.ts.map +1 -1
- package/dist/utilities/module.utility.js +9 -12
- package/dist/utilities/module.utility.js.map +1 -1
- package/dist/utilities/performance.utility.d.ts +2 -2
- package/dist/utilities/performance.utility.d.ts.map +1 -1
- package/dist/utilities/promise.utility.d.ts +3 -11
- package/dist/utilities/promise.utility.d.ts.map +1 -1
- package/dist/utilities/promise.utility.js +1 -9
- package/dist/utilities/promise.utility.js.map +1 -1
- package/dist/utilities/retry.utility.d.ts +4 -22
- package/dist/utilities/retry.utility.d.ts.map +1 -1
- package/dist/utilities/retry.utility.js +2 -20
- package/dist/utilities/retry.utility.js.map +1 -1
- package/package.json +62 -22
- package/dist/modules/logger/application-logger.d.ts +0 -21
- package/dist/modules/logger/application-logger.d.ts.map +0 -1
- package/dist/modules/logger/application-logger.js +0 -66
- package/dist/modules/logger/application-logger.js.map +0 -1
- package/dist/modules/logger/default-logger.d.ts +0 -52
- package/dist/modules/logger/default-logger.d.ts.map +0 -1
- package/dist/modules/logger/default-logger.js.map +0 -1
- package/dist/modules/logger/formatters/deep-splat.d.ts +0 -23
- package/dist/modules/logger/formatters/deep-splat.d.ts.map +0 -1
- package/dist/modules/logger/formatters/deep-splat.js +0 -88
- package/dist/modules/logger/formatters/deep-splat.js.map +0 -1
- package/dist/modules/logger/formatters/index.d.ts.map +0 -1
- package/dist/modules/logger/formatters/index.js.map +0 -1
- package/dist/modules/logger/hf-logger.d.ts +0 -27
- package/dist/modules/logger/hf-logger.d.ts.map +0 -1
- package/dist/modules/logger/hf-logger.js +0 -82
- package/dist/modules/logger/hf-logger.js.map +0 -1
- package/dist/modules/logger/transports/dgram.transport.d.ts.map +0 -1
- package/dist/modules/logger/transports/dgram.transport.js.map +0 -1
- package/dist/modules/logger/transports/index.d.ts.map +0 -1
- package/dist/modules/logger/transports/index.js.map +0 -1
- package/dist/modules/logger/types.d.ts +0 -16
- package/dist/modules/logger/types.d.ts.map +0 -1
- package/dist/modules/logger/types.js +0 -30
- package/dist/modules/logger/types.js.map +0 -1
- /package/dist/modules/logger/{formatters → formatting}/index.d.ts +0 -0
- /package/dist/modules/logger/{formatters → formatting}/index.js +0 -0
- /package/dist/modules/logger/{transports → winston/transports}/index.d.ts +0 -0
- /package/dist/modules/logger/{transports → winston/transports}/index.js +0 -0
package/README.md
CHANGED
|
@@ -1,2968 +1,187 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<br />
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# :fire: IGNIS - `@venizia/ignis-helpers`
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
+
**The infrastructure layer: logging, Redis, queues, storage, crypto, networking.**
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
[](https://ignis.venizia.ai/extensions/helpers/)
|
|
10
|
+
[](https://www.npmjs.com/package/@venizia/ignis-helpers)
|
|
11
|
+
[](LICENSE.md)
|
|
12
|
+
[](https://bun.sh)
|
|
13
|
+
[](https://www.typescriptlang.org/)
|
|
12
14
|
|
|
13
|
-
[
|
|
15
|
+
[Documentation](https://ignis.venizia.ai/extensions/helpers/) •
|
|
16
|
+
[Logger](https://ignis.venizia.ai/extensions/helpers/logger/) •
|
|
17
|
+
[Core API](https://ignis.venizia.ai/references/) •
|
|
18
|
+
[Changelog](https://ignis.venizia.ai/changelogs/)
|
|
14
19
|
|
|
15
20
|
</div>
|
|
16
21
|
|
|
17
|
-
## Highlights
|
|
18
|
-
|
|
19
|
-
| | Feature | |
|
|
20
|
-
| :---: | :--- | :--- |
|
|
21
|
-
| **1** | **14+ Production Modules** | Logger, Redis, Queue, Storage, Crypto, Network, and more |
|
|
22
|
-
| **2** | **Pluggable Architecture** | Install only what you use via optional peer deps |
|
|
23
|
-
| **3** | **Sub-Path Imports** | Tree-shake heavy modules with `@venizia/ignis-helpers/bullmq`, `/minio`, `/kafka`, etc. |
|
|
24
|
-
| **4** | **Consistent API** | Every helper extends `BaseHelper` with scoped logging |
|
|
25
|
-
| **5** | **HfLogger** | Zero-allocation ring buffer logger for hot paths (~100-300ns) |
|
|
26
|
-
| **6** | **Snowflake UID** | 70-bit distributed IDs at ~4M/sec/worker |
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## At a Glance
|
|
31
|
-
|
|
32
|
-
The 5 most common imports:
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { LoggerFactory, RedisSingleHelper, AES } from '@venizia/ignis-helpers';
|
|
36
|
-
import { BullMQHelper } from '@venizia/ignis-helpers/bullmq';
|
|
37
|
-
import { MinioHelper } from '@venizia/ignis-helpers/minio';
|
|
38
|
-
```
|
|
39
|
-
|
|
40
22
|
---
|
|
41
23
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- [Installation](#installation)
|
|
45
|
-
- [Module Overview](#module-overview)
|
|
46
|
-
- [Logger](#logger)
|
|
47
|
-
- [Logger Class](#logger-class)
|
|
48
|
-
- [Log Levels](#log-levels)
|
|
49
|
-
- [LoggerFactory Caching](#loggerfactory-caching)
|
|
50
|
-
- [Custom Logger](#custom-logger)
|
|
51
|
-
- [Transport Types](#transport-types)
|
|
52
|
-
- [HfLogger (High-Frequency Logger)](#hflogger-high-frequency-logger)
|
|
53
|
-
- [Logger Environment Variables](#logger-environment-variables)
|
|
54
|
-
- [Redis](#redis)
|
|
55
|
-
- [Single Instance](#single-instance)
|
|
56
|
-
- [Cluster](#cluster)
|
|
57
|
-
- [String Operations](#string-operations)
|
|
58
|
-
- [Hash Operations](#hash-operations)
|
|
59
|
-
- [Batch Operations](#batch-operations)
|
|
60
|
-
- [Key Operations](#key-operations)
|
|
61
|
-
- [RedisJSON Operations](#redisjson-operations)
|
|
62
|
-
- [Pub/Sub with Compression](#pubsub-with-compression)
|
|
63
|
-
- [Raw Command Execution](#raw-command-execution)
|
|
64
|
-
- [Connection Management](#connection-management)
|
|
65
|
-
- [Redis Event Lifecycle](#redis-event-lifecycle)
|
|
66
|
-
- [Reconnection Behavior](#reconnection-behavior)
|
|
67
|
-
- [Queue](#queue)
|
|
68
|
-
- [BullMQ](#bullmq)
|
|
69
|
-
- [In-Memory Queue](#in-memory-queue)
|
|
70
|
-
- [MQTT](#mqtt)
|
|
71
|
-
- [Kafka (Experimental)](#kafka-experimental)
|
|
72
|
-
- [Storage](#storage)
|
|
73
|
-
- [Common Interface (IStorageHelper)](#common-interface-istoragehelper)
|
|
74
|
-
- [Security Validation](#security-validation)
|
|
75
|
-
- [MinIO (S3-Compatible)](#minio-s3-compatible)
|
|
76
|
-
- [Disk](#disk)
|
|
77
|
-
- [Memory Storage](#memory-storage)
|
|
78
|
-
- [Crypto](#crypto)
|
|
79
|
-
- [AES](#aes)
|
|
80
|
-
- [RSA](#rsa)
|
|
81
|
-
- [ECDH](#ecdh)
|
|
82
|
-
- [Crypto Utility (hash)](#crypto-utility-hash)
|
|
83
|
-
- [Cron](#cron)
|
|
84
|
-
- [Socket.IO](#socketio)
|
|
85
|
-
- [Server](#socketio-server)
|
|
86
|
-
- [Client](#socketio-client)
|
|
87
|
-
- [Authentication Flow](#socketio-authentication-flow)
|
|
88
|
-
- [WebSocket (Bun Native)](#websocket-bun-native)
|
|
89
|
-
- [Server Setup](#websocket-server-setup)
|
|
90
|
-
- [Messaging API](#websocket-messaging-api)
|
|
91
|
-
- [Encryption Support](#websocket-encryption-support)
|
|
92
|
-
- [WebSocket Authentication Flow](#websocket-authentication-flow)
|
|
93
|
-
- [Network](#network)
|
|
94
|
-
- [HTTP Clients](#http-clients)
|
|
95
|
-
- [TCP](#tcp)
|
|
96
|
-
- [UDP](#udp)
|
|
97
|
-
- [UID (Snowflake)](#uid-snowflake)
|
|
98
|
-
- [Bit Layout](#bit-layout)
|
|
99
|
-
- [API Reference](#uid-api-reference)
|
|
100
|
-
- [Throughput and Limits](#throughput-and-limits)
|
|
101
|
-
- [Environment](#environment)
|
|
102
|
-
- [Error](#error)
|
|
103
|
-
- [Worker Thread](#worker-thread)
|
|
104
|
-
- [Common Utilities](#common-utilities)
|
|
105
|
-
- [Date Utility](#date-utility)
|
|
106
|
-
- [Parse Utility](#parse-utility)
|
|
107
|
-
- [Promise Utility](#promise-utility)
|
|
108
|
-
- [Request Utility](#request-utility)
|
|
109
|
-
- [Performance Utility](#performance-utility)
|
|
110
|
-
- [Module Utility](#module-utility)
|
|
111
|
-
- [Common Types](#common-types)
|
|
112
|
-
- [Key Patterns](#key-patterns)
|
|
113
|
-
- [Integration with IGNIS Core](#integration-with-ignis-core)
|
|
114
|
-
- [Environment Variables Reference](#environment-variables-reference)
|
|
115
|
-
- [Error Handling Patterns](#error-handling-patterns)
|
|
116
|
-
- [Performance Tips](#performance-tips)
|
|
117
|
-
- [License](#license)
|
|
24
|
+
The pieces every server needs and nobody wants to write again: a scoped logger, Redis, job queues,
|
|
25
|
+
object storage, crypto, cron, sockets, HTTP clients, distributed IDs, worker threads.
|
|
118
26
|
|
|
119
|
-
|
|
27
|
+
Every helper extends `BaseHelper`, so it arrives with a scoped `logger` and an `identifier` already
|
|
28
|
+
wired. `@venizia/ignis` depends on this package - install it directly when you want the helpers
|
|
29
|
+
outside the framework, or when you need one of the sub-paths below.
|
|
120
30
|
|
|
121
|
-
##
|
|
31
|
+
## Install
|
|
122
32
|
|
|
123
33
|
```bash
|
|
124
34
|
bun add @venizia/ignis-helpers
|
|
125
35
|
```
|
|
126
36
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
| Peer Dependency | Required For | Install Command |
|
|
130
|
-
| -------------------------------- | ---------------------------- | --------------------------------------- |
|
|
131
|
-
| `bullmq` | BullMQ queue/worker | `bun add bullmq` |
|
|
132
|
-
| `cron` | Cron job scheduling | `bun add cron` |
|
|
133
|
-
| `minio` | MinIO/S3 storage | `bun add minio` |
|
|
134
|
-
| `mqtt` | MQTT pub/sub | `bun add mqtt` |
|
|
135
|
-
| `socket.io` | Socket.IO server | `bun add socket.io` |
|
|
136
|
-
| `socket.io-client` | Socket.IO client | `bun add socket.io-client` |
|
|
137
|
-
| `@socket.io/redis-adapter` | Socket.IO Redis adapter | `bun add @socket.io/redis-adapter` |
|
|
138
|
-
| `@socket.io/redis-emitter` | Socket.IO Redis emitter | `bun add @socket.io/redis-emitter` |
|
|
139
|
-
| `axios` | Axios HTTP client | `bun add axios` |
|
|
140
|
-
| `@platformatic/kafka` | Kafka producer/consumer | `bun add @platformatic/kafka` |
|
|
141
|
-
|
|
142
|
-
**Sub-path imports** are available for tree-shaking heavy optional dependencies:
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
import { BullMQHelper } from '@venizia/ignis-helpers/bullmq';
|
|
146
|
-
import { MQTTClientHelper } from '@venizia/ignis-helpers/mqtt';
|
|
147
|
-
import { MinioHelper } from '@venizia/ignis-helpers/minio';
|
|
148
|
-
import { SocketIOServerHelper } from '@venizia/ignis-helpers/socket-io';
|
|
149
|
-
import { AxiosNetworkRequest } from '@venizia/ignis-helpers/axios';
|
|
150
|
-
import { CronHelper } from '@venizia/ignis-helpers/cron';
|
|
151
|
-
import { KafkaProducerHelper, KafkaConsumerHelper, KafkaAdminHelper } from '@venizia/ignis-helpers/kafka';
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Core dependencies** (always installed):
|
|
155
|
-
|
|
156
|
-
| Dependency | Purpose |
|
|
157
|
-
| ------------------------------------------------------------- | ---------------------------------------- |
|
|
158
|
-
| `winston` + `winston-daily-rotate-file` + `winston-transport` | Logger transports |
|
|
159
|
-
| `ioredis` | Redis client |
|
|
160
|
-
| `dayjs` | Date/time utilities |
|
|
161
|
-
| `hono` | HTTP framework types, JSX re-exports |
|
|
162
|
-
| `drizzle-orm` | ORM types (used across IGNIS) |
|
|
163
|
-
| `lodash` | Utility functions |
|
|
164
|
-
| `reflect-metadata` | Decorator metadata |
|
|
165
|
-
| `@venizia/ignis-inversion` | IoC container, `@inject`/`@injectable` |
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## Module Overview
|
|
170
|
-
|
|
171
|
-
| Module | Description |
|
|
172
|
-
| -------------------- | --------------------------------------------------------------------------------------------- |
|
|
173
|
-
| **Logger** | Winston-based logging with daily file rotation, scoped loggers, JSON/text formats, and UDP transport |
|
|
174
|
-
| **HfLogger** | High-frequency zero-allocation ring buffer logger for hot paths (~100--300ns) |
|
|
175
|
-
| **Redis** | Single instance and Cluster support via IoRedis with auto-reconnect, pub/sub, JSON commands, and zlib compression |
|
|
176
|
-
| **Queue (BullMQ)** | Redis-backed distributed job queue with producer/worker roles |
|
|
177
|
-
| **Queue (Internal)** | In-memory generator-based queue with state machine (WAITING/PROCESSING/LOCKED/SETTLED) |
|
|
178
|
-
| **Queue (MQTT)** | MQTT client for IoT and lightweight pub/sub messaging |
|
|
179
|
-
| **Queue (Kafka)** | Kafka producer, consumer, and admin via `@platformatic/kafka` (experimental) |
|
|
180
|
-
| **Storage (MinIO)** | S3-compatible object storage with bucket management and file upload |
|
|
181
|
-
| **Storage (Disk)** | Local filesystem storage with the same `IStorageHelper` interface |
|
|
182
|
-
| **Storage (Memory)** | In-memory key-value storage helper |
|
|
183
|
-
| **Crypto (AES)** | AES-256-CBC and AES-256-GCM encryption/decryption with file support |
|
|
184
|
-
| **Crypto (RSA)** | RSA key pair generation and public/private key encryption |
|
|
185
|
-
| **Crypto (ECDH)** | ECDH P-256 key exchange with HKDF-derived AES-256-GCM (Web Crypto API) |
|
|
186
|
-
| **Cron** | Cron job scheduling with modification and duplication |
|
|
187
|
-
| **Socket.IO** | Socket.IO server and client helpers with Redis adapter, auth flow, and room management |
|
|
188
|
-
| **WebSocket** | Bun-native WebSocket server with Redis pub/sub, encryption support, room/user management |
|
|
189
|
-
| **Network (HTTP)** | HTTP request clients with Axios and native fetch backends |
|
|
190
|
-
| **Network (TCP)** | TCP server and client with TLS support, auth flow, and client state tracking |
|
|
191
|
-
| **Network (UDP)** | UDP client with multicast support |
|
|
192
|
-
| **UID** | Snowflake ID generator (48-10-12 bit layout) with Base62 encoding, ~4M IDs/sec/worker |
|
|
193
|
-
| **Environment** | Environment detection (`NODE_ENV`) and prefixed env var management |
|
|
194
|
-
| **Error** | Standardized `ApplicationError` with status codes and message codes |
|
|
195
|
-
| **Worker Thread** | Worker pool management, worker bus for inter-thread messaging |
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
## Logger
|
|
200
|
-
|
|
201
|
-
Winston-based logging system with console, daily-rotated file, and UDP (dgram) transports.
|
|
202
|
-
|
|
203
|
-
### Logger Class
|
|
204
|
-
|
|
205
|
-
The primary logger with scope-based caching and `[Scope]` prefixed output.
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
import { Logger, LoggerFactory } from '@venizia/ignis-helpers';
|
|
209
|
-
|
|
210
|
-
// Get a logger (cached by scope)
|
|
211
|
-
const logger = Logger.get('UserService');
|
|
212
|
-
logger.info('User created'); // [UserService] User created
|
|
213
|
-
logger.error('Something failed'); // [UserService] Something failed
|
|
214
|
-
|
|
215
|
-
// Create a method-scoped child logger
|
|
216
|
-
logger.for('createUser').info('Validating input');
|
|
217
|
-
// [UserService-createUser] Validating input
|
|
218
|
-
|
|
219
|
-
// Using LoggerFactory (array-based scope)
|
|
220
|
-
const log = LoggerFactory.getLogger(['OrderService', 'v2']);
|
|
221
|
-
log.info('Processing order'); // [OrderService-v2] Processing order
|
|
222
|
-
|
|
223
|
-
// Custom Winston logger instance
|
|
224
|
-
import { defineCustomLogger } from '@venizia/ignis-helpers';
|
|
225
|
-
const customWinston = defineCustomLogger({ /* ... */ });
|
|
226
|
-
const customScopedLogger = Logger.get('MyScope', customWinston);
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Log Levels
|
|
230
|
-
|
|
231
|
-
The logger defines a custom level hierarchy. Lower numeric priority means higher severity:
|
|
232
|
-
|
|
233
|
-
| Level | Priority | Description | Color |
|
|
234
|
-
| --------- | -------- | ------------------------ | ------- |
|
|
235
|
-
| `error` | 0 | Error conditions | Red |
|
|
236
|
-
| `alert` | 0 | Alert conditions | Red |
|
|
237
|
-
| `emerg` | 0 | Emergency conditions | Red |
|
|
238
|
-
| `warn` | 1 | Warning conditions | Yellow |
|
|
239
|
-
| `info` | 2 | Informational messages | Green |
|
|
240
|
-
| `http` | 3 | HTTP request logging | Magenta |
|
|
241
|
-
| `verbose` | 4 | Verbose output | Gray |
|
|
242
|
-
| `debug` | 5 | Debug messages (requires `DEBUG=true`) | Blue |
|
|
243
|
-
| `silly` | 6 | Most verbose | Gray |
|
|
244
|
-
|
|
245
|
-
**Debug level behavior:** The `debug` method is gated at module load time. It only produces output when:
|
|
246
|
-
|
|
247
|
-
1. `DEBUG` env var is truthy, AND
|
|
248
|
-
2. `NODE_ENV` is one of the common environments (`local`, `debug`, `development`, `alpha`, `beta`, `staging`, `production`) or listed in `APP_ENV_EXTRA_LOG_ENVS`.
|
|
249
|
-
|
|
250
|
-
This means calling `logger.debug()` in production with `DEBUG=false` has **zero runtime cost** -- the check is pre-computed once at module load.
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
// Available log methods on Logger:
|
|
254
|
-
logger.debug('msg', ...args); // Only when DEBUG=true
|
|
255
|
-
logger.info('msg', ...args);
|
|
256
|
-
logger.warn('msg', ...args);
|
|
257
|
-
logger.error('msg', ...args);
|
|
258
|
-
logger.emerg('msg', ...args);
|
|
259
|
-
logger.log('http', 'msg', ...args); // Generic method for any level
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### LoggerFactory Caching
|
|
263
|
-
|
|
264
|
-
`Logger.get(scope)` uses a `Map<string, Logger>` for global caching. The same scope always returns the same Logger instance:
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
const a = Logger.get('UserService');
|
|
268
|
-
const b = Logger.get('UserService');
|
|
269
|
-
console.log(a === b); // true -- same cached instance
|
|
270
|
-
|
|
271
|
-
// Method-scoped loggers are also cached
|
|
272
|
-
const c = a.for('create');
|
|
273
|
-
const d = a.for('create');
|
|
274
|
-
console.log(c === d); // true -- cached as 'UserService-create'
|
|
275
|
-
|
|
276
|
-
// Custom loggers use a separate cache key ('scope:custom')
|
|
277
|
-
const custom = Logger.get('MyScope', customWinstonLogger);
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Custom Logger
|
|
281
|
-
|
|
282
|
-
Build a fully custom Winston logger with file rotation and UDP transports:
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
import {
|
|
286
|
-
defineCustomLogger,
|
|
287
|
-
defineLogFormatter,
|
|
288
|
-
defineJsonLoggerFormatter,
|
|
289
|
-
definePrettyLoggerFormatter,
|
|
290
|
-
LoggerFormats,
|
|
291
|
-
} from '@venizia/ignis-helpers';
|
|
292
|
-
|
|
293
|
-
// Create a custom formatter
|
|
294
|
-
const myFormatter = defineLogFormatter({
|
|
295
|
-
label: 'MyApp',
|
|
296
|
-
format: 'json', // 'json' or 'text'
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// Or use specific formatters directly
|
|
300
|
-
const jsonFormat = defineJsonLoggerFormatter({ label: 'MyApp' });
|
|
301
|
-
// Combines: label, timestamp, splat, errors(stack), json, colorize
|
|
302
|
-
|
|
303
|
-
const prettyFormat = definePrettyLoggerFormatter({ label: 'MyApp' });
|
|
304
|
-
// Combines: simple, label, timestamp, splat, align, colorize, printf, errors(stack)
|
|
305
|
-
// Output: "2025-01-01T00:00:00.000Z [MyApp] info: message"
|
|
306
|
-
|
|
307
|
-
// Full custom logger with all transport options
|
|
308
|
-
const myLogger = defineCustomLogger({
|
|
309
|
-
// Optional: override default log levels
|
|
310
|
-
logLevels: {
|
|
311
|
-
error: 0,
|
|
312
|
-
warn: 1,
|
|
313
|
-
info: 2,
|
|
314
|
-
debug: 5,
|
|
315
|
-
},
|
|
316
|
-
// Optional: override default colors
|
|
317
|
-
logColors: {
|
|
318
|
-
error: 'red',
|
|
319
|
-
warn: 'yellow',
|
|
320
|
-
info: 'green',
|
|
321
|
-
debug: 'blue',
|
|
322
|
-
},
|
|
323
|
-
// Optional: custom formatter (defaults to applicationLogFormatter)
|
|
324
|
-
loggerFormatter: myFormatter,
|
|
325
|
-
transports: {
|
|
326
|
-
info: {
|
|
327
|
-
file: {
|
|
328
|
-
folder: './logs',
|
|
329
|
-
prefix: 'myapp',
|
|
330
|
-
frequency: '1h', // Rotation frequency
|
|
331
|
-
maxSize: '100m', // Max file size
|
|
332
|
-
maxFiles: '5d', // Retention period
|
|
333
|
-
datePattern: 'YYYYMMDD_HH',
|
|
334
|
-
},
|
|
335
|
-
dgram: {
|
|
336
|
-
socketOptions: { type: 'udp4' },
|
|
337
|
-
host: '127.0.0.1',
|
|
338
|
-
port: 9999,
|
|
339
|
-
label: 'myapp-info',
|
|
340
|
-
levels: ['info', 'warn'], // Only forward these levels via UDP
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
error: {
|
|
344
|
-
file: {
|
|
345
|
-
folder: './logs',
|
|
346
|
-
prefix: 'myapp',
|
|
347
|
-
},
|
|
348
|
-
dgram: {
|
|
349
|
-
socketOptions: { type: 'udp4' },
|
|
350
|
-
host: '127.0.0.1',
|
|
351
|
-
port: 9999,
|
|
352
|
-
label: 'myapp-error',
|
|
353
|
-
levels: ['error', 'emerg'],
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Transport Types
|
|
361
|
-
|
|
362
|
-
**Console Transport:**
|
|
363
|
-
Always active. Level: `debug`. Provides colorized output to stdout.
|
|
364
|
-
|
|
365
|
-
**DailyRotateFile Transport:**
|
|
366
|
-
Configured per info/error channel. Automatically rotates files based on time and size.
|
|
367
|
-
|
|
368
|
-
```
|
|
369
|
-
./logs/APP-info-20250101_14.log # Rotated hourly by default
|
|
370
|
-
./logs/APP-error-20250101_14.log
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
| Option | Default | Description |
|
|
374
|
-
| ------------- | -------------- | ---------------------------------------- |
|
|
375
|
-
| `frequency` | `1h` | How often to rotate |
|
|
376
|
-
| `maxSize` | `100m` | Maximum size before forced rotation |
|
|
377
|
-
| `maxFiles` | `5d` | How long to retain rotated files |
|
|
378
|
-
| `datePattern` | `YYYYMMDD_HH` | Date format in rotated filenames |
|
|
37
|
+
Nothing heavy comes with it. Every module with a third-party backend declares that backend as an
|
|
38
|
+
**optional peer** - install only the ones you use.
|
|
379
39
|
|
|
380
|
-
|
|
381
|
-
Forwards log messages over UDP to a remote collector. Only activates when host, port, label, and levels are all provided.
|
|
40
|
+
## Usage
|
|
382
41
|
|
|
383
42
|
```typescript
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
import {
|
|
387
|
-
|
|
388
|
-
const transport = new DgramTransport({
|
|
389
|
-
label: 'my-service',
|
|
390
|
-
host: '10.0.0.1',
|
|
391
|
-
port: 5140,
|
|
392
|
-
levels: ['error', 'warn'], // Only forward these levels
|
|
393
|
-
socketOptions: { type: 'udp4' },
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// Or use the safe factory (returns null if any required option is missing)
|
|
397
|
-
const safeTransport = DgramTransport.fromPartial({
|
|
398
|
-
host: process.env.LOG_UDP_HOST, // If undefined, returns null
|
|
399
|
-
port: parseInt(process.env.LOG_UDP_PORT ?? '0'),
|
|
400
|
-
label: 'my-service',
|
|
401
|
-
levels: ['error'],
|
|
402
|
-
socketOptions: { type: 'udp4' },
|
|
403
|
-
});
|
|
404
|
-
```
|
|
43
|
+
// src/index.ts - the entrypoint, before anything logs
|
|
44
|
+
import { LoggerFactory } from '@venizia/ignis-helpers';
|
|
45
|
+
import { PinoLogger } from '@venizia/ignis-helpers/pino';
|
|
405
46
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
```
|
|
409
|
-
<timestamp> [<label>] <level> <message>
|
|
47
|
+
LoggerFactory.use({ provider: PinoLogger });
|
|
410
48
|
```
|
|
411
49
|
|
|
412
|
-
### HfLogger (High-Frequency Logger)
|
|
413
|
-
|
|
414
|
-
Zero-allocation logger using a pre-allocated `SharedArrayBuffer` ring buffer for latency-critical hot paths (~100--300ns per log). Designed for trading engines, real-time pipelines, and any code path where Winston's overhead is unacceptable.
|
|
415
|
-
|
|
416
|
-
**Architecture:**
|
|
417
|
-
|
|
418
|
-
- 64K-entry ring buffer (65,536 slots x 256 bytes = 16MB `SharedArrayBuffer`)
|
|
419
|
-
- Lock-free writes via atomic increment of write index
|
|
420
|
-
- Entries are written in binary format, flushed asynchronously to disk
|
|
421
|
-
|
|
422
|
-
**Entry format (256 bytes per entry):**
|
|
423
|
-
|
|
424
|
-
| Offset | Size | Field |
|
|
425
|
-
| ------- | --------- | -------------------------------------------- |
|
|
426
|
-
| 0--7 | 8 bytes | Timestamp (BigInt64, nanosecond precision) |
|
|
427
|
-
| 8 | 1 byte | Level (0=debug, 1=info, 2=warn, 3=error, 4=emerg) |
|
|
428
|
-
| 9--40 | 32 bytes | Scope (fixed-width, padded) |
|
|
429
|
-
| 41--255 | 215 bytes | Message (fixed-width, truncated if longer) |
|
|
430
|
-
|
|
431
50
|
```typescript
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// --- Initialization phase (once, before hot path) ---
|
|
435
|
-
|
|
436
|
-
// Get a cached logger instance (scope bytes are pre-computed)
|
|
437
|
-
const logger = HfLogger.get('OrderEngine');
|
|
438
|
-
|
|
439
|
-
// Pre-encode messages at init time (ZERO allocation in hot path)
|
|
440
|
-
const MSG_SENT = HfLogger.encodeMessage('Order sent');
|
|
441
|
-
const MSG_FILLED = HfLogger.encodeMessage('Order filled');
|
|
442
|
-
const MSG_REJECTED = HfLogger.encodeMessage('Order rejected');
|
|
443
|
-
|
|
444
|
-
// --- Hot path (~100-300ns per call, zero allocation) ---
|
|
445
|
-
logger.log('info', MSG_SENT);
|
|
446
|
-
logger.log('info', MSG_FILLED);
|
|
447
|
-
logger.log('error', MSG_REJECTED);
|
|
448
|
-
|
|
449
|
-
// --- Background flush (separate context / interval) ---
|
|
450
|
-
const flusher = new HfLogFlusher();
|
|
451
|
-
flusher.start(100); // flush every 100ms
|
|
452
|
-
|
|
453
|
-
// Or flush manually
|
|
454
|
-
await flusher.flush();
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
**HfLogger levels:**
|
|
458
|
-
|
|
459
|
-
| Level | Numeric Value |
|
|
460
|
-
| ------- | ------------- |
|
|
461
|
-
| `debug` | 0 |
|
|
462
|
-
| `info` | 1 |
|
|
463
|
-
| `warn` | 2 |
|
|
464
|
-
| `error` | 3 |
|
|
465
|
-
| `emerg` | 4 |
|
|
466
|
-
|
|
467
|
-
**Key characteristics:**
|
|
468
|
-
|
|
469
|
-
- Scope cache: `Map<string, Uint8Array>` -- scope strings are encoded to fixed 32-byte arrays once
|
|
470
|
-
- Message cache: `Map<string, Uint8Array>` -- pre-encoded messages avoid `TextEncoder.encode()` in hot path
|
|
471
|
-
- Ring buffer wraps around using bitmask: `writeIndex++ & (BUFFER_SIZE - 1)`
|
|
472
|
-
- The flusher reads from flushIndex to writeIndex, decoding each entry back to text
|
|
473
|
-
|
|
474
|
-
### Logger Environment Variables
|
|
475
|
-
|
|
476
|
-
| Variable | Default | Description |
|
|
477
|
-
| ----------------------------------- | -------------- | ---------------------------------------------- |
|
|
478
|
-
| `APP_ENV_LOGGER_FOLDER_PATH` | `./` | Directory for log files |
|
|
479
|
-
| `APP_ENV_LOGGER_FORMAT` | `text` | Log format: `json` or `text` |
|
|
480
|
-
| `APP_ENV_LOGGER_FILE_FREQUENCY` | `1h` | File rotation frequency |
|
|
481
|
-
| `APP_ENV_LOGGER_FILE_MAX_SIZE` | `100m` | Max file size before rotation |
|
|
482
|
-
| `APP_ENV_LOGGER_FILE_MAX_FILES` | `5d` | Max days to retain log files |
|
|
483
|
-
| `APP_ENV_LOGGER_FILE_DATE_PATTERN` | `YYYYMMDD_HH` | Date pattern for rotated file names |
|
|
484
|
-
| `APP_ENV_LOGGER_DGRAM_HOST` | -- | UDP transport host |
|
|
485
|
-
| `APP_ENV_LOGGER_DGRAM_PORT` | -- | UDP transport port |
|
|
486
|
-
| `APP_ENV_LOGGER_DGRAM_LABEL` | -- | UDP transport label |
|
|
487
|
-
| `APP_ENV_LOGGER_DGRAM_LEVELS` | -- | Comma-separated log levels to forward via UDP |
|
|
488
|
-
| `DEBUG` | -- | Set to any truthy value to enable `debug` level output |
|
|
489
|
-
| `APP_ENV_EXTRA_LOG_ENVS` | -- | Comma-separated extra `NODE_ENV` values that enable debug |
|
|
490
|
-
| `APP_ENV_APPLICATION_NAME` | `APP` | Application name prefix for log files and labels |
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Redis
|
|
495
|
-
|
|
496
|
-
Full-featured Redis client wrapper around IoRedis supporting single instances and clusters.
|
|
497
|
-
|
|
498
|
-
### Single Instance
|
|
51
|
+
// anywhere else - provider-agnostic
|
|
52
|
+
import { createRedisHelper, ILogger, LoggerFactory, RedisModes } from '@venizia/ignis-helpers';
|
|
499
53
|
|
|
500
|
-
|
|
501
|
-
import { RedisSingleHelper } from '@venizia/ignis-helpers';
|
|
54
|
+
const logger: ILogger = LoggerFactory.getLogger(['CacheService']);
|
|
502
55
|
|
|
503
|
-
const redis =
|
|
504
|
-
|
|
56
|
+
const redis = createRedisHelper({
|
|
57
|
+
mode: RedisModes.SINGLE,
|
|
58
|
+
name: 'cache',
|
|
505
59
|
host: '127.0.0.1',
|
|
506
60
|
port: 6379,
|
|
507
|
-
password: 'secret',
|
|
508
|
-
database: 0, // Redis DB index (default: 0)
|
|
509
|
-
autoConnect: true, // Connect immediately (default: true, false = lazyConnect)
|
|
510
|
-
maxRetry: 5, // Max reconnect attempts (-1 = unlimited, 0 = no retry, default: 0)
|
|
511
|
-
onReady: ({ name }) => console.log(`${name} ready`),
|
|
512
|
-
onError: ({ error }) => console.error(error),
|
|
513
|
-
});
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
### Cluster
|
|
517
|
-
|
|
518
|
-
```typescript
|
|
519
|
-
import { RedisClusterHelper } from '@venizia/ignis-helpers';
|
|
520
|
-
|
|
521
|
-
const cluster = new RedisClusterHelper({
|
|
522
|
-
name: 'redis-cluster',
|
|
523
|
-
nodes: [
|
|
524
|
-
{ host: 'node1.redis.example.com', port: 6379 },
|
|
525
|
-
{ host: 'node2.redis.example.com', port: 6379 },
|
|
526
|
-
{ host: 'node3.redis.example.com', port: 6379 },
|
|
527
|
-
],
|
|
528
|
-
clusterOptions: {
|
|
529
|
-
redisOptions: { password: 'secret' },
|
|
530
|
-
// scaleReads: 'slave', // Optional: read from replicas
|
|
531
|
-
// natMap: { ... }, // If behind NAT/proxy
|
|
532
|
-
},
|
|
533
|
-
onReady: ({ name }) => console.log(`${name} cluster ready`),
|
|
534
|
-
});
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
Both `RedisSingleHelper` and `RedisClusterHelper` extend `AbstractRedisHelper`, which provides all data operations. They differ only in how the underlying IoRedis client is created.
|
|
538
|
-
|
|
539
|
-
### String Operations
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
// SET -- auto JSON.stringify for objects
|
|
543
|
-
await redis.set({ key: 'user:1', value: { name: 'Alice', age: 30 } });
|
|
544
|
-
await redis.set({ key: 'user:1', value: 'simple-string' });
|
|
545
|
-
await redis.set({ key: 'counter', value: 42, options: { log: true } }); // Enable debug logging
|
|
546
|
-
|
|
547
|
-
// GET -- returns raw string
|
|
548
|
-
const raw = await redis.get({ key: 'user:1' }); // '{"name":"Alice","age":30}'
|
|
549
|
-
|
|
550
|
-
// GET with custom transform
|
|
551
|
-
const parsed = await redis.get({
|
|
552
|
-
key: 'user:1',
|
|
553
|
-
transform: (str: string) => JSON.parse(str),
|
|
554
|
-
}); // { name: 'Alice', age: 30 }
|
|
555
|
-
|
|
556
|
-
// getString -- alias for get()
|
|
557
|
-
const str = await redis.getString({ key: 'token' });
|
|
558
|
-
|
|
559
|
-
// getObject -- auto JSON.parse
|
|
560
|
-
const user = await redis.getObject({ key: 'user:1' }); // { name: 'Alice', age: 30 }
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Hash Operations
|
|
564
|
-
|
|
565
|
-
```typescript
|
|
566
|
-
// HSET -- set multiple hash fields at once
|
|
567
|
-
await redis.hset({ key: 'hash:1', value: { field1: 'data1', field2: 'data2' } });
|
|
568
|
-
// Returns number of NEW fields added
|
|
569
|
-
|
|
570
|
-
// HGETALL -- get all fields as object
|
|
571
|
-
const all = await redis.hgetall({ key: 'hash:1' });
|
|
572
|
-
// { field1: 'data1', field2: 'data2' }
|
|
573
|
-
|
|
574
|
-
// HGETALL with transform
|
|
575
|
-
const transformed = await redis.hgetall({
|
|
576
|
-
key: 'hash:1',
|
|
577
|
-
transform: (obj) => Object.entries(obj),
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
// Aliases (PascalCase)
|
|
581
|
-
await redis.hSet({ key: 'hash:1', value: { field: 'data' } });
|
|
582
|
-
const result = await redis.hGetAll({ key: 'hash:1' });
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
### Batch Operations
|
|
586
|
-
|
|
587
|
-
```typescript
|
|
588
|
-
// MSET -- set multiple keys at once (auto JSON.stringify)
|
|
589
|
-
await redis.mset({
|
|
590
|
-
payload: [
|
|
591
|
-
{ key: 'k1', value: { a: 1 } },
|
|
592
|
-
{ key: 'k2', value: { b: 2 } },
|
|
593
|
-
{ key: 'k3', value: 'plain-string' },
|
|
594
|
-
],
|
|
595
|
-
options: { log: true },
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// MGET -- get multiple keys at once
|
|
599
|
-
const values = await redis.mget({ keys: ['k1', 'k2', 'k3'] });
|
|
600
|
-
// ['{"a":1}', '{"b":2}', '"plain-string"']
|
|
601
|
-
|
|
602
|
-
// MGET with transform
|
|
603
|
-
const objects = await redis.mget({
|
|
604
|
-
keys: ['k1', 'k2'],
|
|
605
|
-
transform: (str: string) => JSON.parse(str),
|
|
606
|
-
});
|
|
607
|
-
// [{ a: 1 }, { b: 2 }]
|
|
608
|
-
|
|
609
|
-
// Convenience methods
|
|
610
|
-
const strings = await redis.getStrings({ keys: ['k1', 'k2'] });
|
|
611
|
-
const objs = await redis.getObjects({ keys: ['k1', 'k2'] });
|
|
612
|
-
|
|
613
|
-
// Aliases (PascalCase)
|
|
614
|
-
await redis.mSet({ payload: [{ key: 'k', value: 'v' }] });
|
|
615
|
-
const vals = await redis.mGet({ keys: ['k'] });
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### Key Operations
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
// KEYS -- find keys by pattern
|
|
622
|
-
const keys = await redis.keys({ key: 'user:*' });
|
|
623
|
-
// ['user:1', 'user:2', 'user:3']
|
|
624
|
-
|
|
625
|
-
// DEL -- delete one or more keys
|
|
626
|
-
await redis.del({ keys: ['user:1', 'user:2'] });
|
|
627
|
-
// Returns number of keys deleted
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
### RedisJSON Operations
|
|
631
|
-
|
|
632
|
-
These operations require the [RedisJSON](https://redis.io/docs/stack/json/) module installed on your Redis server.
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
// JSON.SET -- set a JSON document at a path
|
|
636
|
-
await redis.jSet({ key: 'doc:1', path: '$', value: { name: 'Alice', age: 30, tags: [] } });
|
|
637
|
-
// Returns 'OK'
|
|
638
|
-
|
|
639
|
-
// JSON.GET -- retrieve JSON at a path
|
|
640
|
-
const doc = await redis.jGet({ key: 'doc:1', path: '$' });
|
|
641
|
-
// Returns the JSON value
|
|
642
|
-
|
|
643
|
-
// JSON.GET with default path
|
|
644
|
-
const full = await redis.jGet({ key: 'doc:1' }); // path defaults to '$'
|
|
645
|
-
|
|
646
|
-
// JSON.DEL -- delete a path within a JSON document
|
|
647
|
-
await redis.jDelete({ key: 'doc:1', path: '$.age' });
|
|
648
|
-
// Returns number of paths deleted
|
|
649
|
-
|
|
650
|
-
// JSON.NUMINCRBY -- increment a numeric value
|
|
651
|
-
await redis.jNumberIncreaseBy({ key: 'doc:1', path: '$.age', value: 1 });
|
|
652
|
-
// Returns new value as string
|
|
653
|
-
|
|
654
|
-
// JSON.STRAPPEND -- append to a string value
|
|
655
|
-
await redis.jStringAppend({ key: 'doc:1', path: '$.name', value: ' Smith' });
|
|
656
|
-
// Returns array of new string lengths
|
|
657
|
-
|
|
658
|
-
// JSON.ARRAPPEND -- push to an array
|
|
659
|
-
await redis.jPush({ key: 'doc:1', path: '$.tags', value: 'admin' });
|
|
660
|
-
// Returns array of new array lengths
|
|
661
|
-
|
|
662
|
-
// JSON.ARRPOP -- pop from an array
|
|
663
|
-
const popped = await redis.jPop({ key: 'doc:1', path: '$.tags' });
|
|
664
|
-
// Returns the popped value
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
### Pub/Sub with Compression
|
|
668
|
-
|
|
669
|
-
```typescript
|
|
670
|
-
// Subscribe to a topic
|
|
671
|
-
redis.subscribe({ topic: 'events' });
|
|
672
|
-
|
|
673
|
-
// Listen for messages on the underlying IoRedis client
|
|
674
|
-
redis.client.on('message', (channel: string, message: Buffer | string) => {
|
|
675
|
-
// If compressed, decompress:
|
|
676
|
-
// const data = JSON.parse(zlib.inflateSync(message).toString());
|
|
677
|
-
console.log(channel, message);
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
// Publish to one or more topics
|
|
681
|
-
await redis.publish({
|
|
682
|
-
topics: ['events', 'notifications'],
|
|
683
|
-
payload: { type: 'user.created', userId: '123' },
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
// Publish with zlib compression (deflateSync)
|
|
687
|
-
await redis.publish({
|
|
688
|
-
topics: ['events'],
|
|
689
|
-
payload: { type: 'user.created', userId: '123' },
|
|
690
|
-
useCompress: true, // Compresses payload with zlib.deflateSync before publishing
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
// Unsubscribe from a topic
|
|
694
|
-
redis.unsubscribe({ topic: 'events' });
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
### Raw Command Execution
|
|
698
|
-
|
|
699
|
-
Execute any Redis command directly:
|
|
700
|
-
|
|
701
|
-
```typescript
|
|
702
|
-
// Simple command with no parameters
|
|
703
|
-
const pong = await redis.execute<string>('PING');
|
|
704
|
-
// 'PONG'
|
|
705
|
-
|
|
706
|
-
// Command with parameters
|
|
707
|
-
const result = await redis.execute<string>('SET', ['mykey', 'myvalue']);
|
|
708
|
-
|
|
709
|
-
// Complex command
|
|
710
|
-
const info = await redis.execute<string>('INFO', ['server']);
|
|
711
|
-
|
|
712
|
-
// Custom module commands
|
|
713
|
-
const custom = await redis.execute<any>('FT.SEARCH', ['my-index', '*']);
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
### Connection Management
|
|
717
|
-
|
|
718
|
-
```typescript
|
|
719
|
-
// Connect (if using lazyConnect / autoConnect: false)
|
|
720
|
-
const connected = await redis.connect();
|
|
721
|
-
// Returns true if status is 'ready', false if already connecting/ready
|
|
722
|
-
|
|
723
|
-
// Disconnect gracefully (sends QUIT command)
|
|
724
|
-
const disconnected = await redis.disconnect();
|
|
725
|
-
// Returns true if quit was successful
|
|
726
|
-
|
|
727
|
-
// Check connection status
|
|
728
|
-
redis.client.status; // 'wait' | 'connecting' | 'connect' | 'ready' | 'close' | 'reconnecting' | 'end'
|
|
729
|
-
|
|
730
|
-
// Ping
|
|
731
|
-
await redis.ping(); // 'PONG'
|
|
732
|
-
|
|
733
|
-
// Access the underlying IoRedis client directly
|
|
734
|
-
const ioRedisClient = redis.getClient(); // Redis (single) or Cluster instance
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
### Redis Event Lifecycle
|
|
738
|
-
|
|
739
|
-
Events are fired in order during the connection lifecycle:
|
|
740
|
-
|
|
741
|
-
| Order | Callback | Triggered When | Receives |
|
|
742
|
-
| ----- | --------------- | -------------------------------------- | ---------------------- |
|
|
743
|
-
| 1 | `onInitialized` | Helper instance created (synchronous) | `{ name, helper }` |
|
|
744
|
-
| 2 | `onConnected` | Redis TCP connection established | `{ name, helper }` |
|
|
745
|
-
| 3 | `onReady` | Redis is ready to accept commands | `{ name, helper }` |
|
|
746
|
-
| -- | `onError` | Any Redis error | `{ name, helper, error }` |
|
|
747
|
-
|
|
748
|
-
Additionally, the client logs `reconnecting` events automatically.
|
|
749
|
-
|
|
750
|
-
### Reconnection Behavior
|
|
751
|
-
|
|
752
|
-
The Redis helper (`AbstractRedisHelper`) uses exponential backoff for reconnection:
|
|
753
|
-
|
|
754
|
-
```
|
|
755
|
-
Strategy: Math.max(Math.min(attempt * 2000, 5000), 1000) ms
|
|
756
|
-
|
|
757
|
-
Attempt 1: 1000ms (min clamp)
|
|
758
|
-
Attempt 2: 2000ms
|
|
759
|
-
Attempt 3: 4000ms
|
|
760
|
-
Attempt 4: 5000ms (max clamp)
|
|
761
|
-
Attempt 5: 5000ms (max clamp)
|
|
762
|
-
...continues at 5000ms until maxRetry is reached
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
- `maxRetry: 0` -- no retries (default)
|
|
766
|
-
- `maxRetry: -1` -- unlimited retries (not recommended for production without monitoring)
|
|
767
|
-
- `maxRetry: 5` -- retry 5 times then give up
|
|
768
|
-
|
|
769
|
-
The helper also sets `maxRetriesPerRequest: null` (required by BullMQ) and `showFriendlyErrorStack: true`.
|
|
770
|
-
|
|
771
|
-
---
|
|
772
|
-
|
|
773
|
-
## Queue
|
|
774
|
-
|
|
775
|
-
### BullMQ
|
|
776
|
-
|
|
777
|
-
Redis-backed distributed job queue using BullMQ. Import from the sub-path to avoid bundling BullMQ when unused.
|
|
778
|
-
|
|
779
|
-
**Producer setup:**
|
|
780
|
-
|
|
781
|
-
```typescript
|
|
782
|
-
import { BullMQHelper } from '@venizia/ignis-helpers/bullmq';
|
|
783
|
-
|
|
784
|
-
const producer = BullMQHelper.newInstance({
|
|
785
|
-
queueName: 'email-queue',
|
|
786
|
-
identifier: 'email-producer',
|
|
787
|
-
role: 'queue', // 'queue' = producer, 'worker' = consumer
|
|
788
|
-
redisConnection: redis, // IRedisHelper instance
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
// Add jobs to the queue
|
|
792
|
-
await producer.queue.add('send-welcome', {
|
|
793
|
-
to: 'user@example.com',
|
|
794
|
-
template: 'welcome',
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Add with BullMQ job options
|
|
798
|
-
await producer.queue.add('send-invoice', data, {
|
|
799
|
-
delay: 5000, // Delay 5 seconds
|
|
800
|
-
attempts: 3, // Retry up to 3 times
|
|
801
|
-
backoff: { type: 'exponential', delay: 1000 },
|
|
802
|
-
priority: 1, // Lower = higher priority
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// Cleanup
|
|
806
|
-
await producer.close();
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
**Worker setup:**
|
|
810
|
-
|
|
811
|
-
```typescript
|
|
812
|
-
const worker = BullMQHelper.newInstance({
|
|
813
|
-
queueName: 'email-queue',
|
|
814
|
-
identifier: 'email-worker',
|
|
815
|
-
role: 'worker',
|
|
816
|
-
redisConnection: redis,
|
|
817
|
-
numberOfWorker: 3, // Concurrency (default: 1)
|
|
818
|
-
lockDuration: 90 * 60 * 1000, // Job lock duration: 90 minutes (default)
|
|
819
|
-
onWorkerData: async (job) => {
|
|
820
|
-
// Process the job
|
|
821
|
-
await sendEmail(job.data);
|
|
822
|
-
return { sent: true };
|
|
823
|
-
},
|
|
824
|
-
onWorkerDataCompleted: async (job, result) => {
|
|
825
|
-
console.log(`Job ${job.id} completed`, result);
|
|
826
|
-
},
|
|
827
|
-
onWorkerDataFail: async (job, error) => {
|
|
828
|
-
console.error(`Job ${job?.id} failed`, error);
|
|
829
|
-
},
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
// Cleanup
|
|
833
|
-
await worker.close();
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
**Default job options (configured on the queue):**
|
|
837
|
-
|
|
838
|
-
- `removeOnComplete: true` -- clean up completed jobs automatically
|
|
839
|
-
- `removeOnFail: true` -- clean up failed jobs automatically
|
|
840
|
-
|
|
841
|
-
**Redis connection note:** BullMQ requires `maxRetriesPerRequest: null`. The Redis helper sets this automatically. The BullMQ helper calls `redisConnection.getClient().duplicate()` for both queue and worker connections so they don't interfere with your main Redis client.
|
|
842
|
-
|
|
843
|
-
**Supported roles:**
|
|
844
|
-
|
|
845
|
-
- `'queue'` -- creates a `Queue` instance (producer)
|
|
846
|
-
- `'worker'` -- creates a `Worker` instance (consumer)
|
|
847
|
-
|
|
848
|
-
### In-Memory Queue
|
|
849
|
-
|
|
850
|
-
Generator-based in-memory queue with a state machine for synchronous sequential processing. Useful for ordering operations that must be processed one at a time.
|
|
851
|
-
|
|
852
|
-
**State machine:**
|
|
853
|
-
|
|
854
|
-
```
|
|
855
|
-
WAITING --(enqueue + autoDispatch)--> PROCESSING --(message done)--> WAITING
|
|
856
|
-
| |
|
|
857
|
-
+--(lock())--> LOCKED +--(settle request + empty)--> SETTLED
|
|
858
|
-
|
|
|
859
|
-
+--(unlock())--> WAITING
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
| State | Description |
|
|
863
|
-
| ---------------- | ---------------------------------------- |
|
|
864
|
-
| `000_WAITING` | Idle, waiting for next message |
|
|
865
|
-
| `100_PROCESSING` | Currently processing a message |
|
|
866
|
-
| `200_LOCKED` | Paused, no new messages processed |
|
|
867
|
-
| `300_SETTLED` | Terminal state, no more items accepted |
|
|
868
|
-
|
|
869
|
-
```typescript
|
|
870
|
-
import { QueueHelper, QueueStatuses } from '@venizia/ignis-helpers';
|
|
871
|
-
|
|
872
|
-
const queue = new QueueHelper<{ userId: string }>({
|
|
873
|
-
identifier: 'sync-processor',
|
|
874
|
-
autoDispatch: true, // Auto-process on enqueue (default: true)
|
|
875
|
-
onMessage: async ({ queueElement }) => {
|
|
876
|
-
console.log('Processing:', queueElement.payload);
|
|
877
|
-
// { isLocked: true, payload: { userId: 'user-1' } }
|
|
878
|
-
},
|
|
879
|
-
onDataEnqueue: async ({ identifier, queueElement }) => {
|
|
880
|
-
console.log('Enqueued:', queueElement.payload);
|
|
881
|
-
},
|
|
882
|
-
onDataDequeue: async ({ identifier, queueElement }) => {
|
|
883
|
-
console.log('Dequeued:', queueElement.payload);
|
|
884
|
-
},
|
|
885
|
-
onStateChange: async ({ from, to }) => {
|
|
886
|
-
console.log(`State: ${from} -> ${to}`);
|
|
887
|
-
},
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
// Enqueue items (auto-dispatched one at a time)
|
|
891
|
-
await queue.enqueue({ userId: 'user-1' });
|
|
892
|
-
await queue.enqueue({ userId: 'user-2' });
|
|
893
|
-
|
|
894
|
-
// Flow control
|
|
895
|
-
queue.lock(); // Pause processing (LOCKED state)
|
|
896
|
-
queue.unlock({ shouldProcessNextElement: true }); // Resume + process next
|
|
897
|
-
queue.unlock({ shouldProcessNextElement: false }); // Resume without processing
|
|
898
|
-
|
|
899
|
-
// Settle -- no more items accepted; transitions to SETTLED when empty
|
|
900
|
-
queue.settle();
|
|
901
|
-
|
|
902
|
-
// Check state
|
|
903
|
-
queue.getState(); // Current state string
|
|
904
|
-
queue.isSettled(); // true when SETTLED and storage is empty
|
|
905
|
-
queue.getTotalEvent(); // Total items ever enqueued
|
|
906
|
-
queue.getProcessingEvents(); // Set of currently processing elements
|
|
907
|
-
|
|
908
|
-
// Close -- settle + return generator
|
|
909
|
-
queue.close();
|
|
910
|
-
|
|
911
|
-
// Manual dispatch (when autoDispatch: false)
|
|
912
|
-
queue.nextMessage();
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
### MQTT
|
|
916
|
-
|
|
917
|
-
Lightweight MQTT pub/sub client for IoT messaging.
|
|
918
|
-
|
|
919
|
-
```typescript
|
|
920
|
-
import { MQTTClientHelper } from '@venizia/ignis-helpers/mqtt';
|
|
921
|
-
|
|
922
|
-
const mqtt = new MQTTClientHelper({
|
|
923
|
-
identifier: 'iot-client',
|
|
924
|
-
url: 'mqtt://broker.example.com:1883',
|
|
925
|
-
options: {
|
|
926
|
-
username: 'device',
|
|
927
|
-
password: 'secret',
|
|
928
|
-
clientId: 'sensor-001',
|
|
929
|
-
keepalive: 60,
|
|
930
|
-
reconnectPeriod: 1000,
|
|
931
|
-
},
|
|
932
|
-
onConnect: () => console.log('MQTT connected'),
|
|
933
|
-
onDisconnect: () => console.log('MQTT disconnected'),
|
|
934
|
-
onError: (error) => console.error('MQTT error:', error),
|
|
935
|
-
onClose: (error) => console.log('MQTT closed', error),
|
|
936
|
-
onMessage: ({ topic, message }) => {
|
|
937
|
-
console.log(`[${topic}]`, message.toString());
|
|
938
|
-
},
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// Subscribe to topics
|
|
942
|
-
await mqtt.subscribe({ topics: ['sensors/temperature', 'sensors/humidity'] });
|
|
943
|
-
|
|
944
|
-
// Publish a message
|
|
945
|
-
await mqtt.publish({ topic: 'commands/led', message: 'ON' });
|
|
946
|
-
await mqtt.publish({ topic: 'data/json', message: JSON.stringify({ temp: 23.5 }) });
|
|
947
|
-
await mqtt.publish({ topic: 'data/binary', message: Buffer.from([0x01, 0x02]) });
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
The MQTT client auto-connects on construction. If the client is already established, `configure()` is a no-op. Subscribe/publish throw `ApplicationError` if the client is not connected.
|
|
951
|
-
|
|
952
|
-
### Kafka (Experimental)
|
|
953
|
-
|
|
954
|
-
Apache Kafka helpers built on `@platformatic/kafka`, imported from the `@venizia/ignis-helpers/kafka` sub-path: `KafkaProducerHelper`, `KafkaConsumerHelper`, `KafkaAdminHelper`, and `KafkaSchemaRegistryHelper`, all extending a shared `BaseKafkaHelper` (scoped logging, per-broker health tracking, graceful shutdown, `newInstance()` factory pattern).
|
|
955
|
-
|
|
956
|
-
```typescript
|
|
957
|
-
import { KafkaProducerHelper, KafkaAcks } from '@venizia/ignis-helpers/kafka';
|
|
958
|
-
|
|
959
|
-
const producer = KafkaProducerHelper.newInstance({
|
|
960
|
-
identifier: 'my-producer',
|
|
961
|
-
bootstrapBrokers: ['localhost:9092'],
|
|
962
|
-
acks: KafkaAcks.ALL, // -1 = all in-sync replicas
|
|
963
61
|
});
|
|
964
62
|
|
|
965
|
-
await
|
|
966
|
-
|
|
967
|
-
});
|
|
63
|
+
await redis.set({ key: 'user:1', value: { name: 'IGNIS' }, options: { expiresIn: 60_000 } });
|
|
64
|
+
const user = await redis.get<{ name: string }>({ key: 'user:1', transform: JSON.parse });
|
|
968
65
|
|
|
969
|
-
|
|
66
|
+
logger.for('warmup').info('Cache warmed | user: %j', user);
|
|
970
67
|
```
|
|
971
68
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
---
|
|
975
|
-
|
|
976
|
-
## Storage
|
|
69
|
+
## Sub-path exports
|
|
977
70
|
|
|
978
|
-
|
|
71
|
+
**This is the one thing to get right.** A helper that value-imports an optional peer is reachable
|
|
72
|
+
**only** from its own sub-path. The root barrel is peer-free by construction: `import
|
|
73
|
+
'@venizia/ignis-helpers'` works with zero peers installed, and your bundler only ever packages the
|
|
74
|
+
backends you imported by name.
|
|
979
75
|
|
|
980
|
-
|
|
76
|
+
| Sub-path | Exports | Peer dependencies to install |
|
|
77
|
+
| :--- | :--- | :--- |
|
|
78
|
+
| `/winston` | `WinstonLogger` | `winston`, `winston-transport`, `winston-daily-rotate-file` |
|
|
79
|
+
| `/pino` | `PinoLogger` | `pino` (+ `pino-pretty` for text format, `pino-roll` for file rotation) |
|
|
80
|
+
| `/bullmq` | `BullMQHelper` | `bullmq` |
|
|
81
|
+
| `/mqtt` | `MQTTClientHelper` | `mqtt` |
|
|
82
|
+
| `/kafka` | `KafkaProducerHelper`, `KafkaConsumerHelper`, `KafkaAdminHelper`, ... | `@platformatic/kafka` |
|
|
83
|
+
| `/minio` | `MinioHelper` | `minio` |
|
|
84
|
+
| `/bun-s3` | `BunS3Helper` | none - Bun's native S3 client |
|
|
85
|
+
| `/socket-io` | `SocketIOServerHelper`, `SocketIOClientHelper` | `socket.io`, `socket.io-client` (+ `@socket.io/redis-adapter`, `@socket.io/redis-emitter` for multi-node) |
|
|
86
|
+
| `/axios` | `AxiosFetcher`, `AxiosNetworkRequest` | `axios` |
|
|
87
|
+
| `/cron` | `CronHelper` | `cron` |
|
|
88
|
+
| `/hashicorp-vault` | `HashiCorpVaultHelper` | `node-vault` |
|
|
89
|
+
| `/dotenv-vault` | `DotenvVaultHelper` | `@dotenvx/dotenvx` |
|
|
981
90
|
|
|
982
|
-
```
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const storage = new MinioHelper({
|
|
1080
|
-
endPoint: 'minio.example.com',
|
|
1081
|
-
port: 9000,
|
|
1082
|
-
useSSL: false,
|
|
1083
|
-
accessKey: 'minioadmin',
|
|
1084
|
-
secretKey: 'minioadmin',
|
|
1085
|
-
// All minio.ClientOptions are supported
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
// Bucket operations
|
|
1089
|
-
await storage.createBucket({ name: 'uploads' });
|
|
1090
|
-
const exists = await storage.isBucketExists({ name: 'uploads' });
|
|
1091
|
-
const buckets = await storage.getBuckets(); // IBucketInfo[]
|
|
1092
|
-
const bucket = await storage.getBucket({ name: 'uploads' }); // IBucketInfo | null
|
|
1093
|
-
await storage.removeBucket({ name: 'empty-bucket' });
|
|
1094
|
-
|
|
1095
|
-
// Upload files
|
|
1096
|
-
const results = await storage.upload({
|
|
1097
|
-
bucket: 'uploads',
|
|
1098
|
-
files: [{
|
|
1099
|
-
originalName: 'photo.jpg',
|
|
1100
|
-
mimetype: 'image/jpeg',
|
|
1101
|
-
buffer: fileBuffer,
|
|
1102
|
-
size: fileBuffer.length,
|
|
1103
|
-
folderPath: 'images', // Optional subfolder
|
|
1104
|
-
}],
|
|
1105
|
-
// Optional: customize naming
|
|
1106
|
-
normalizeNameFn: ({ originalName, folderPath }) =>
|
|
1107
|
-
`${folderPath}/${Date.now()}-${originalName}`,
|
|
1108
|
-
// Optional: customize link generation
|
|
1109
|
-
normalizeLinkFn: ({ bucketName, normalizeName }) =>
|
|
1110
|
-
`https://cdn.example.com/${bucketName}/${normalizeName}`,
|
|
1111
|
-
});
|
|
1112
|
-
// [{ bucketName: 'uploads', objectName: 'images/photo.jpg', link: '/static-assets/uploads/...' }]
|
|
1113
|
-
|
|
1114
|
-
// Retrieve file as readable stream
|
|
1115
|
-
const stream = await storage.getFile({
|
|
1116
|
-
bucket: 'uploads',
|
|
1117
|
-
name: 'images/photo.jpg',
|
|
1118
|
-
options: {
|
|
1119
|
-
versionId: 'abc123', // Optional version
|
|
1120
|
-
SSECustomerAlgorithm: 'AES256', // Optional SSE
|
|
1121
|
-
SSECustomerKey: 'base64key',
|
|
1122
|
-
SSECustomerKeyMD5: 'md5hash',
|
|
1123
|
-
},
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
// Get file metadata
|
|
1127
|
-
const stat = await storage.getStat({ bucket: 'uploads', name: 'images/photo.jpg' });
|
|
1128
|
-
// { size: 12345, metadata: { ... }, lastModified: Date, etag: '...', versionId: '...' }
|
|
1129
|
-
|
|
1130
|
-
// List objects
|
|
1131
|
-
const objects = await storage.listObjects({
|
|
1132
|
-
bucket: 'uploads',
|
|
1133
|
-
prefix: 'images/',
|
|
1134
|
-
useRecursive: true,
|
|
1135
|
-
maxKeys: 100,
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
// Delete
|
|
1139
|
-
await storage.removeObject({ bucket: 'uploads', name: 'images/photo.jpg' });
|
|
1140
|
-
await storage.removeObjects({ bucket: 'uploads', names: ['file1.jpg', 'file2.jpg'] });
|
|
1141
|
-
|
|
1142
|
-
// Access underlying minio.Client for advanced operations
|
|
1143
|
-
storage.client.presignedGetObject('uploads', 'photo.jpg', 3600); // Pre-signed URL
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
### Disk
|
|
1147
|
-
|
|
1148
|
-
Local filesystem storage with the same interface. Buckets are directories; objects are files.
|
|
1149
|
-
|
|
1150
|
-
```typescript
|
|
1151
|
-
import { DiskHelper } from '@venizia/ignis-helpers';
|
|
1152
|
-
|
|
1153
|
-
const storage = new DiskHelper({
|
|
1154
|
-
basePath: './data/storage', // Base directory (created automatically if missing)
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
// All IStorageHelper operations work identically
|
|
1158
|
-
await storage.createBucket({ name: 'uploads' });
|
|
1159
|
-
// Creates: ./data/storage/uploads/
|
|
1160
|
-
|
|
1161
|
-
await storage.upload({
|
|
1162
|
-
bucket: 'uploads',
|
|
1163
|
-
files: [{ originalName: 'doc.pdf', mimetype: 'application/pdf', buffer: buf, size: buf.length }],
|
|
1164
|
-
});
|
|
1165
|
-
// Writes: ./data/storage/uploads/doc.pdf
|
|
1166
|
-
// Returns: [{ bucketName: 'uploads', objectName: 'doc.pdf', link: '/static-resources/uploads/doc.pdf' }]
|
|
1167
|
-
|
|
1168
|
-
const stream = await storage.getFile({ bucket: 'uploads', name: 'doc.pdf' });
|
|
1169
|
-
// Returns: fs.createReadStream(...)
|
|
1170
|
-
|
|
1171
|
-
const stat = await storage.getStat({ bucket: 'uploads', name: 'doc.pdf' });
|
|
1172
|
-
// { size: 12345, lastModified: Date, metadata: { mimetype: 'application/pdf' } }
|
|
1173
|
-
|
|
1174
|
-
// List with prefix and recursion
|
|
1175
|
-
const objects = await storage.listObjects({
|
|
1176
|
-
bucket: 'uploads',
|
|
1177
|
-
prefix: 'reports/',
|
|
1178
|
-
useRecursive: true,
|
|
1179
|
-
maxKeys: 50,
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
// Remove bucket (must be empty)
|
|
1183
|
-
await storage.removeBucket({ name: 'uploads' });
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
The Disk helper auto-detects MIME types from file extensions using a built-in map (20+ common types, falls back to `application/octet-stream`).
|
|
1187
|
-
|
|
1188
|
-
### Memory Storage
|
|
1189
|
-
|
|
1190
|
-
Simple in-memory key-value storage (not implementing `IStorageHelper`). Useful for testing, caching, and temporary data.
|
|
1191
|
-
|
|
1192
|
-
```typescript
|
|
1193
|
-
import { MemoryStorageHelper } from '@venizia/ignis-helpers';
|
|
1194
|
-
|
|
1195
|
-
// Type-safe container
|
|
1196
|
-
const cache = MemoryStorageHelper.newInstance<{
|
|
1197
|
-
token: string;
|
|
1198
|
-
user: { name: string; role: string };
|
|
1199
|
-
settings: Record<string, boolean>;
|
|
1200
|
-
}>();
|
|
1201
|
-
|
|
1202
|
-
cache.set('token', 'abc123');
|
|
1203
|
-
cache.set('user', { name: 'Alice', role: 'admin' });
|
|
1204
|
-
cache.set('settings', { darkMode: true });
|
|
1205
|
-
|
|
1206
|
-
const token = cache.get<string>('token'); // 'abc123'
|
|
1207
|
-
const user = cache.get<{ name: string }>('user'); // { name: 'Alice', role: 'admin' }
|
|
1208
|
-
|
|
1209
|
-
cache.isBound('token'); // true
|
|
1210
|
-
cache.isBound('missing'); // false
|
|
1211
|
-
|
|
1212
|
-
cache.keys(); // ['token', 'user', 'settings']
|
|
1213
|
-
cache.getContainer(); // Full internal object
|
|
1214
|
-
|
|
1215
|
-
cache.clear(); // Reset to empty object
|
|
1216
|
-
cache.keys(); // []
|
|
1217
|
-
```
|
|
1218
|
-
|
|
1219
|
-
---
|
|
1220
|
-
|
|
1221
|
-
## Crypto
|
|
1222
|
-
|
|
1223
|
-
### AES
|
|
1224
|
-
|
|
1225
|
-
Symmetric encryption with AES-256-CBC or AES-256-GCM.
|
|
1226
|
-
|
|
1227
|
-
**Algorithm comparison:**
|
|
1228
|
-
|
|
1229
|
-
| Feature | AES-256-CBC | AES-256-GCM |
|
|
1230
|
-
| ---------------- | ------------------------------ | ------------------------------------------- |
|
|
1231
|
-
| Confidentiality | Yes | Yes |
|
|
1232
|
-
| Authentication | No (encrypt-only) | Yes (AEAD -- built-in auth tag) |
|
|
1233
|
-
| Auth tag size | N/A | 16 bytes |
|
|
1234
|
-
| Performance | Slightly faster | Slightly slower (auth overhead) |
|
|
1235
|
-
| Use case | Legacy compatibility | **Recommended** for new code |
|
|
1236
|
-
| Encrypted format | `[IV(16B)][ciphertext]` | `[IV(16B)][authTag(16B)][ciphertext]` |
|
|
1237
|
-
|
|
1238
|
-
```typescript
|
|
1239
|
-
import { AES } from '@venizia/ignis-helpers';
|
|
1240
|
-
|
|
1241
|
-
// Create instance with chosen algorithm
|
|
1242
|
-
const aesCbc = AES.withAlgorithm('aes-256-cbc');
|
|
1243
|
-
const aesGcm = AES.withAlgorithm('aes-256-gcm'); // Recommended
|
|
1244
|
-
|
|
1245
|
-
// Encrypt string
|
|
1246
|
-
const encrypted = aesGcm.encrypt({
|
|
1247
|
-
message: 'Hello, World!',
|
|
1248
|
-
secret: 'my-secret-key', // Auto-padded/truncated to 32 bytes (256 bits)
|
|
1249
|
-
});
|
|
1250
|
-
// Returns base64-encoded string: [IV][AuthTag][Ciphertext]
|
|
1251
|
-
|
|
1252
|
-
// Decrypt string
|
|
1253
|
-
const decrypted = aesGcm.decrypt({
|
|
1254
|
-
message: encrypted,
|
|
1255
|
-
secret: 'my-secret-key',
|
|
1256
|
-
});
|
|
1257
|
-
// 'Hello, World!'
|
|
1258
|
-
|
|
1259
|
-
// File encryption
|
|
1260
|
-
const encryptedContent = aesGcm.encryptFile({
|
|
1261
|
-
absolutePath: '/path/to/file.txt',
|
|
1262
|
-
secret: 'my-secret-key',
|
|
1263
|
-
});
|
|
1264
|
-
// Returns base64-encoded encrypted file content
|
|
1265
|
-
|
|
1266
|
-
const decryptedContent = aesGcm.decryptFile({
|
|
1267
|
-
absolutePath: '/path/to/encrypted.txt',
|
|
1268
|
-
secret: 'my-secret-key',
|
|
1269
|
-
});
|
|
1270
|
-
// Returns original file content as string
|
|
1271
|
-
|
|
1272
|
-
// Advanced options
|
|
1273
|
-
const result = aesGcm.encrypt({
|
|
1274
|
-
message: 'data',
|
|
1275
|
-
secret: 'key',
|
|
1276
|
-
opts: {
|
|
1277
|
-
iv: customIV, // Custom IV (default: crypto.randomBytes(16))
|
|
1278
|
-
inputEncoding: 'utf-8', // Input encoding (default: 'utf-8')
|
|
1279
|
-
outputEncoding: 'base64', // Output encoding (default: 'base64')
|
|
1280
|
-
doThrow: false, // If false, returns original message on error (default: true)
|
|
1281
|
-
},
|
|
1282
|
-
});
|
|
1283
|
-
```
|
|
1284
|
-
|
|
1285
|
-
**Key normalization:** The secret key is automatically padded with null bytes (`0x00`) or truncated to exactly 32 bytes (256 bits). You can pass a passphrase of any length.
|
|
1286
|
-
|
|
1287
|
-
### RSA
|
|
1288
|
-
|
|
1289
|
-
Asymmetric encryption using RSA with DER key format.
|
|
1290
|
-
|
|
1291
|
-
```typescript
|
|
1292
|
-
import { RSA } from '@venizia/ignis-helpers';
|
|
1293
|
-
|
|
1294
|
-
const rsa = RSA.withAlgorithm();
|
|
1295
|
-
|
|
1296
|
-
// Generate DER key pair
|
|
1297
|
-
const { publicKey, privateKey } = rsa.generateDERKeyPair({ modulus: 2048 });
|
|
1298
|
-
// publicKey: Buffer (DER, SPKI format)
|
|
1299
|
-
// privateKey: Buffer (DER, PKCS8 format)
|
|
1300
|
-
|
|
1301
|
-
// Convert to base64 for storage/transmission
|
|
1302
|
-
const pubKeyB64 = publicKey.toString('base64');
|
|
1303
|
-
const privKeyB64 = privateKey.toString('base64');
|
|
1304
|
-
|
|
1305
|
-
// Encrypt with public key
|
|
1306
|
-
const encrypted = rsa.encrypt({
|
|
1307
|
-
message: 'Secret message',
|
|
1308
|
-
secret: pubKeyB64,
|
|
1309
|
-
// opts: {
|
|
1310
|
-
// inputEncoding: { key: 'base64', message: 'utf-8' }, // defaults
|
|
1311
|
-
// outputEncoding: 'base64', // default
|
|
1312
|
-
// doThrow: true, // default
|
|
1313
|
-
// }
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
// Decrypt with private key
|
|
1317
|
-
const decrypted = rsa.decrypt({
|
|
1318
|
-
message: encrypted,
|
|
1319
|
-
secret: privKeyB64,
|
|
1320
|
-
// opts: {
|
|
1321
|
-
// inputEncoding: { key: 'base64', message: 'base64' }, // defaults
|
|
1322
|
-
// outputEncoding: 'utf-8', // default
|
|
1323
|
-
// doThrow: true, // default
|
|
1324
|
-
// }
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
// Available modulus sizes
|
|
1328
|
-
rsa.generateDERKeyPair({ modulus: 1024 }); // Fast, insecure (testing only)
|
|
1329
|
-
rsa.generateDERKeyPair({ modulus: 2048 }); // Standard (default)
|
|
1330
|
-
rsa.generateDERKeyPair({ modulus: 4096 }); // High security, slower
|
|
1331
|
-
```
|
|
1332
|
-
|
|
1333
|
-
### ECDH
|
|
1334
|
-
|
|
1335
|
-
Elliptic Curve Diffie-Hellman key exchange with HKDF-derived AES-256-GCM session keys. Uses the Web Crypto API for cross-platform compatibility (Bun and browsers).
|
|
1336
|
-
|
|
1337
|
-
**Algorithm details:**
|
|
1338
|
-
|
|
1339
|
-
- Curve: P-256 (NIST)
|
|
1340
|
-
- Key derivation: HKDF with SHA-256
|
|
1341
|
-
- Cipher: AES-256-GCM with 128-bit auth tag
|
|
1342
|
-
- IV: 12 bytes random per encryption
|
|
1343
|
-
- Salt: 32 bytes (generated or shared)
|
|
1344
|
-
- Info string: `ignis-ecdh-p256-aes-256-gcm-v1` (customizable)
|
|
1345
|
-
|
|
1346
|
-
```typescript
|
|
1347
|
-
import { ECDH } from '@venizia/ignis-helpers';
|
|
1348
|
-
|
|
1349
|
-
const ecdh = ECDH.withAlgorithm();
|
|
1350
|
-
// Or with custom HKDF info string:
|
|
1351
|
-
// const ecdh = ECDH.withAlgorithm({ hkdfInfo: 'my-app-v1' });
|
|
1352
|
-
|
|
1353
|
-
// --- Key Exchange Flow ---
|
|
1354
|
-
|
|
1355
|
-
// 1. Both parties generate key pairs
|
|
1356
|
-
const alice = await ecdh.generateKeyPair();
|
|
1357
|
-
// { keyPair: CryptoKeyPair, publicKeyB64: 'base64-encoded-raw-public-key' }
|
|
1358
|
-
|
|
1359
|
-
const bob = await ecdh.generateKeyPair();
|
|
1360
|
-
|
|
1361
|
-
// 2. Exchange public keys (via network, etc.)
|
|
1362
|
-
const alicePeerKey = await ecdh.importPublicKey({ rawKeyB64: bob.publicKeyB64 });
|
|
1363
|
-
const bobPeerKey = await ecdh.importPublicKey({ rawKeyB64: alice.publicKeyB64 });
|
|
1364
|
-
|
|
1365
|
-
// 3. Derive shared AES-256-GCM keys
|
|
1366
|
-
const aliceSession = await ecdh.deriveAESKey({
|
|
1367
|
-
privateKey: alice.keyPair.privateKey,
|
|
1368
|
-
peerPublicKey: alicePeerKey,
|
|
1369
|
-
// salt is auto-generated if not provided
|
|
1370
|
-
});
|
|
1371
|
-
// { key: CryptoKey, salt: 'base64-encoded-salt' }
|
|
1372
|
-
|
|
1373
|
-
const bobSession = await ecdh.deriveAESKey({
|
|
1374
|
-
privateKey: bob.keyPair.privateKey,
|
|
1375
|
-
peerPublicKey: bobPeerKey,
|
|
1376
|
-
salt: aliceSession.salt, // MUST use same salt to derive same key
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
// 4. Encrypt with Alice's derived key
|
|
1380
|
-
const payload = await ecdh.encrypt({
|
|
1381
|
-
message: 'Hello Bob!',
|
|
1382
|
-
secret: aliceSession.key,
|
|
1383
|
-
opts: {
|
|
1384
|
-
additionalData: 'optional-aad', // Optional authenticated associated data
|
|
1385
|
-
},
|
|
1386
|
-
});
|
|
1387
|
-
// { iv: 'base64-iv', ct: 'base64-ciphertext' }
|
|
1388
|
-
|
|
1389
|
-
// 5. Decrypt with Bob's derived key (same shared secret)
|
|
1390
|
-
const plaintext = await ecdh.decrypt({
|
|
1391
|
-
message: payload,
|
|
1392
|
-
secret: bobSession.key,
|
|
1393
|
-
opts: {
|
|
1394
|
-
additionalData: 'optional-aad', // Must match what was used in encrypt
|
|
1395
|
-
},
|
|
1396
|
-
});
|
|
1397
|
-
// 'Hello Bob!'
|
|
1398
|
-
```
|
|
1399
|
-
|
|
1400
|
-
### Crypto Utility (hash)
|
|
1401
|
-
|
|
1402
|
-
Simple hashing utility for HMAC-SHA256 and MD5:
|
|
1403
|
-
|
|
1404
|
-
```typescript
|
|
1405
|
-
import { hash } from '@venizia/ignis-helpers';
|
|
1406
|
-
|
|
1407
|
-
// HMAC-SHA256 (requires secret)
|
|
1408
|
-
const sha256 = hash('data', {
|
|
1409
|
-
algorithm: 'SHA256',
|
|
1410
|
-
secret: 'hmac-secret',
|
|
1411
|
-
outputType: 'hex', // 'hex' | 'base64' | 'latin1'
|
|
1412
|
-
});
|
|
1413
|
-
|
|
1414
|
-
// MD5 (no secret required)
|
|
1415
|
-
const md5 = hash('data', {
|
|
1416
|
-
algorithm: 'MD5',
|
|
1417
|
-
outputType: 'base64',
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
// SHA256 without secret returns the original text (no-op)
|
|
1421
|
-
const noop = hash('data', { algorithm: 'SHA256', outputType: 'hex' });
|
|
1422
|
-
// 'data'
|
|
1423
|
-
```
|
|
1424
|
-
|
|
1425
|
-
---
|
|
1426
|
-
|
|
1427
|
-
## Cron
|
|
1428
|
-
|
|
1429
|
-
Scheduled job management built on the `cron` package.
|
|
1430
|
-
|
|
1431
|
-
```typescript
|
|
1432
|
-
import { CronHelper } from '@venizia/ignis-helpers/cron';
|
|
1433
|
-
|
|
1434
|
-
const job = CronHelper.newInstance({
|
|
1435
|
-
cronTime: '0 */5 * * * *', // Every 5 minutes
|
|
1436
|
-
onTick: async () => {
|
|
1437
|
-
console.log('Running scheduled task');
|
|
1438
|
-
},
|
|
1439
|
-
autoStart: true,
|
|
1440
|
-
tz: 'America/New_York',
|
|
1441
|
-
errorHandler: (error) => {
|
|
1442
|
-
console.error('Cron error:', error);
|
|
1443
|
-
},
|
|
1444
|
-
});
|
|
1445
|
-
|
|
1446
|
-
// Manual control
|
|
1447
|
-
job.start();
|
|
1448
|
-
|
|
1449
|
-
// Modify schedule at runtime
|
|
1450
|
-
job.modifyCronTime({
|
|
1451
|
-
cronTime: '0 */10 * * * *', // Change to every 10 minutes
|
|
1452
|
-
shouldFireOnTick: true, // Fire immediately after modification
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
// Duplicate with different schedule (same onTick handler)
|
|
1456
|
-
const nightJob = job.duplicate({
|
|
1457
|
-
cronTime: '0 0 2 * * *', // 2:00 AM daily
|
|
1458
|
-
});
|
|
1459
|
-
nightJob.start();
|
|
1460
|
-
|
|
1461
|
-
// Access underlying CronJob instance
|
|
1462
|
-
const nextDate = job.instance.nextDate();
|
|
1463
|
-
```
|
|
1464
|
-
|
|
1465
|
-
---
|
|
1466
|
-
|
|
1467
|
-
## Socket.IO
|
|
1468
|
-
|
|
1469
|
-
Real-time communication helpers supporting both Node.js and Bun runtimes, with Redis adapter for horizontal scaling.
|
|
1470
|
-
|
|
1471
|
-
### Socket.IO Server
|
|
1472
|
-
|
|
1473
|
-
```typescript
|
|
1474
|
-
import { SocketIOServerHelper } from '@venizia/ignis-helpers/socket-io';
|
|
1475
|
-
|
|
1476
|
-
const io = new SocketIOServerHelper({
|
|
1477
|
-
identifier: 'main-ws',
|
|
1478
|
-
runtime: 'node', // or 'bun'
|
|
1479
|
-
server: httpServer, // Node.js HTTP server (for 'node' runtime)
|
|
1480
|
-
// engine: bunEngine, // @socket.io/bun-engine (for 'bun' runtime)
|
|
1481
|
-
serverOptions: {
|
|
1482
|
-
cors: { origin: '*' },
|
|
1483
|
-
path: '/socket.io',
|
|
1484
|
-
},
|
|
1485
|
-
redisConnection: redis, // IRedisHelper instance
|
|
1486
|
-
defaultRooms: ['io-default', 'io-notification'],
|
|
1487
|
-
authenticateTimeout: 10_000, // 10s to authenticate
|
|
1488
|
-
pingInterval: 30_000, // 30s ping interval
|
|
1489
|
-
|
|
1490
|
-
authenticateFn: (handshake) => {
|
|
1491
|
-
const token = handshake.auth?.token;
|
|
1492
|
-
return verifyJWT(token); // return true/false
|
|
1493
|
-
},
|
|
1494
|
-
validateRoomFn: ({ socket, rooms }) => {
|
|
1495
|
-
return rooms.filter(r => isAuthorized(socket, r));
|
|
1496
|
-
},
|
|
1497
|
-
clientConnectedFn: async ({ socket }) => {
|
|
1498
|
-
console.log('Client authenticated:', socket.id);
|
|
1499
|
-
},
|
|
1500
|
-
});
|
|
1501
|
-
|
|
1502
|
-
await io.configure();
|
|
1503
|
-
|
|
1504
|
-
// Send messages
|
|
1505
|
-
io.send({
|
|
1506
|
-
destination: 'room-123', // room, client ID, or omit for broadcast
|
|
1507
|
-
payload: {
|
|
1508
|
-
topic: 'notification',
|
|
1509
|
-
data: { message: 'Hello!' },
|
|
1510
|
-
},
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
// Register custom event handlers
|
|
1514
|
-
io.on({
|
|
1515
|
-
topic: 'custom-event',
|
|
1516
|
-
handler: (data) => console.log(data),
|
|
1517
|
-
});
|
|
1518
|
-
|
|
1519
|
-
// Retrieve connected clients
|
|
1520
|
-
const clients = io.getClients();
|
|
1521
|
-
const client = io.getClients({ id: 'socket-id-123' });
|
|
1522
|
-
|
|
1523
|
-
// Shutdown
|
|
1524
|
-
await io.shutdown();
|
|
1525
|
-
```
|
|
1526
|
-
|
|
1527
|
-
### Socket.IO Client
|
|
1528
|
-
|
|
1529
|
-
```typescript
|
|
1530
|
-
import { SocketIOClientHelper } from '@venizia/ignis-helpers/socket-io';
|
|
1531
|
-
|
|
1532
|
-
const client = new SocketIOClientHelper({
|
|
1533
|
-
identifier: 'app-client',
|
|
1534
|
-
host: 'http://localhost:3000',
|
|
1535
|
-
options: {
|
|
1536
|
-
path: '/socket.io',
|
|
1537
|
-
extraHeaders: { authorization: 'Bearer token123' },
|
|
1538
|
-
},
|
|
1539
|
-
onConnected: () => console.log('Connected'),
|
|
1540
|
-
onAuthenticated: () => console.log('Authenticated'),
|
|
1541
|
-
onDisconnected: (reason) => console.log('Disconnected:', reason),
|
|
1542
|
-
});
|
|
1543
|
-
|
|
1544
|
-
// Authenticate (triggers server-side authenticateFn)
|
|
1545
|
-
client.authenticate();
|
|
1546
|
-
|
|
1547
|
-
// Subscribe to events
|
|
1548
|
-
client.subscribe({
|
|
1549
|
-
event: 'notification',
|
|
1550
|
-
handler: (data) => console.log('Notification:', data),
|
|
1551
|
-
});
|
|
1552
|
-
|
|
1553
|
-
// Subscribe to multiple events
|
|
1554
|
-
client.subscribeMany({
|
|
1555
|
-
events: {
|
|
1556
|
-
'message': (data) => console.log(data),
|
|
1557
|
-
'alert': (data) => console.warn(data),
|
|
1558
|
-
},
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
// Emit events
|
|
1562
|
-
client.emit({ topic: 'chat', data: { text: 'Hello!' } });
|
|
1563
|
-
|
|
1564
|
-
// Room management
|
|
1565
|
-
client.joinRooms({ rooms: ['room-1', 'room-2'] });
|
|
1566
|
-
client.leaveRooms({ rooms: ['room-1'] });
|
|
1567
|
-
|
|
1568
|
-
// Cleanup
|
|
1569
|
-
client.shutdown();
|
|
1570
|
-
```
|
|
1571
|
-
|
|
1572
|
-
### Socket.IO Authentication Flow
|
|
1573
|
-
|
|
1574
|
-
1. Client connects to Socket.IO server
|
|
1575
|
-
2. Server starts authentication timeout (default 10s)
|
|
1576
|
-
3. Client emits `authenticate` event
|
|
1577
|
-
4. Server calls `authenticateFn` with handshake data
|
|
1578
|
-
5. On success: client joins default rooms, receives `authenticated` event, ping interval starts
|
|
1579
|
-
6. On failure: client receives `unauthenticated` event and is disconnected
|
|
1580
|
-
7. On timeout: client is disconnected
|
|
1581
|
-
|
|
1582
|
-
**System events:**
|
|
1583
|
-
|
|
1584
|
-
| Event | Direction | Description |
|
|
1585
|
-
| ----------------- | ------------------ | ---------------------- |
|
|
1586
|
-
| `ping` | Server -> Client | Keepalive |
|
|
1587
|
-
| `connection` | Client -> Server | Initial connection |
|
|
1588
|
-
| `disconnect` | Bidirectional | Disconnection |
|
|
1589
|
-
| `join` | Client -> Server | Join rooms |
|
|
1590
|
-
| `leave` | Client -> Server | Leave rooms |
|
|
1591
|
-
| `authenticate` | Client -> Server | Start authentication |
|
|
1592
|
-
| `authenticated` | Server -> Client | Auth success |
|
|
1593
|
-
| `unauthenticated` | Server -> Client | Auth failure |
|
|
1594
|
-
|
|
1595
|
-
---
|
|
1596
|
-
|
|
1597
|
-
## WebSocket (Bun Native)
|
|
1598
|
-
|
|
1599
|
-
High-performance WebSocket server using Bun's native WebSocket API with Redis pub/sub for multi-instance scaling. Supports optional end-to-end encryption with an outbound transformer pattern.
|
|
1600
|
-
|
|
1601
|
-
### WebSocket Server Setup
|
|
1602
|
-
|
|
1603
|
-
```typescript
|
|
1604
|
-
import { WebSocketServerHelper } from '@venizia/ignis-helpers';
|
|
1605
|
-
|
|
1606
|
-
const ws = new WebSocketServerHelper({
|
|
1607
|
-
identifier: 'realtime',
|
|
1608
|
-
server: bunServer, // Bun server instance (IBunServer)
|
|
1609
|
-
redisConnection: redis, // IRedisHelper instance
|
|
1610
|
-
path: '/ws', // Default: '/ws'
|
|
1611
|
-
authTimeout: 5_000, // 5s to authenticate (default)
|
|
1612
|
-
heartbeatInterval: 30_000, // 30s between heartbeats (default)
|
|
1613
|
-
heartbeatTimeout: 90_000, // 3x interval, disconnect after 3 missed (default)
|
|
1614
|
-
requireEncryption: false, // When true, handshakeFn is required
|
|
1615
|
-
encryptedBatchLimit: 10, // Max concurrent encryption operations (default)
|
|
1616
|
-
defaultRooms: ['ws-default', 'ws-notification'], // Auto-join on auth
|
|
1617
|
-
|
|
1618
|
-
serverOptions: {
|
|
1619
|
-
maxPayloadLength: 128 * 1024, // 128KB (default)
|
|
1620
|
-
idleTimeout: 60, // seconds (default)
|
|
1621
|
-
sendPings: true, // default
|
|
1622
|
-
perMessageDeflate: false,
|
|
1623
|
-
backpressureLimit: 1024 * 1024, // 1MB
|
|
1624
|
-
},
|
|
1625
|
-
|
|
1626
|
-
authenticateFn: async (data) => {
|
|
1627
|
-
const user = await verifyToken(data.token);
|
|
1628
|
-
return user ? { userId: user.id, metadata: { role: user.role } } : null;
|
|
1629
|
-
},
|
|
1630
|
-
validateRoomFn: ({ userId, rooms }) => {
|
|
1631
|
-
return rooms.filter(r => canJoinRoom(userId, r));
|
|
1632
|
-
},
|
|
1633
|
-
clientConnectedFn: async ({ clientId, userId, metadata }) => {
|
|
1634
|
-
console.log(`${userId} connected as ${clientId}`, metadata);
|
|
1635
|
-
},
|
|
1636
|
-
clientDisconnectedFn: async ({ clientId, userId }) => {
|
|
1637
|
-
console.log(`${userId} (${clientId}) disconnected`);
|
|
1638
|
-
},
|
|
1639
|
-
messageHandler: async ({ clientId, userId, message }) => {
|
|
1640
|
-
console.log(`[${clientId}] ${message.event}:`, message.data);
|
|
1641
|
-
},
|
|
1642
|
-
});
|
|
1643
|
-
|
|
1644
|
-
await ws.configure();
|
|
1645
|
-
|
|
1646
|
-
// Get the Bun WebSocket handler for server.upgrade()
|
|
1647
|
-
const handler = ws.getBunWebSocketHandler();
|
|
1648
|
-
// Use in Bun.serve({ websocket: handler, fetch: ... })
|
|
1649
|
-
```
|
|
1650
|
-
|
|
1651
|
-
### WebSocket Messaging API
|
|
1652
|
-
|
|
1653
|
-
```typescript
|
|
1654
|
-
// --- High-level API (local + Redis cross-instance) ---
|
|
1655
|
-
|
|
1656
|
-
// Broadcast to all authenticated clients
|
|
1657
|
-
ws.send({ payload: { topic: 'update', data: { version: 2 } } });
|
|
1658
|
-
|
|
1659
|
-
// Send to specific client
|
|
1660
|
-
ws.send({ destination: clientId, payload: { topic: 'dm', data: { text: 'hi' } } });
|
|
1661
|
-
|
|
1662
|
-
// Send to room
|
|
1663
|
-
ws.send({ destination: 'room-1', payload: { topic: 'chat', data: { msg: 'hello' } } });
|
|
1664
|
-
|
|
1665
|
-
// --- Low-level APIs (local instance only) ---
|
|
1666
|
-
|
|
1667
|
-
ws.sendToClient({ clientId, event: 'ping', data: {} });
|
|
1668
|
-
ws.sendToUser({ userId: 'user-1', event: 'alert', data: { msg: 'hi' } });
|
|
1669
|
-
ws.sendToRoom({ room: 'lobby', event: 'join', data: { user: 'Alice' } });
|
|
1670
|
-
ws.sendToRoom({ room: 'lobby', event: 'msg', data: { text: 'hi' }, exclude: [clientId] });
|
|
1671
|
-
ws.broadcast({ event: 'system', data: { maintenance: true } });
|
|
1672
|
-
ws.broadcast({ event: 'system', data: { maintenance: true }, exclude: ['client-1'] });
|
|
1673
|
-
|
|
1674
|
-
// --- Room Management ---
|
|
1675
|
-
|
|
1676
|
-
ws.joinRoom({ clientId, room: 'my-room' });
|
|
1677
|
-
ws.leaveRoom({ clientId, room: 'my-room' });
|
|
1678
|
-
|
|
1679
|
-
// --- Query ---
|
|
1680
|
-
|
|
1681
|
-
const allClients = ws.getClients(); // Map<string, IWebSocketClient>
|
|
1682
|
-
const oneClient = ws.getClients({ id: clientId }); // IWebSocketClient | undefined
|
|
1683
|
-
const userClients = ws.getClientsByUser({ userId: 'user-1' }); // IWebSocketClient[]
|
|
1684
|
-
const roomClients = ws.getClientsByRoom({ room: 'lobby' }); // IWebSocketClient[]
|
|
1685
|
-
|
|
1686
|
-
await ws.shutdown();
|
|
1687
|
-
```
|
|
1688
|
-
|
|
1689
|
-
### WebSocket Encryption Support
|
|
1690
|
-
|
|
1691
|
-
When `requireEncryption: true`, clients must complete an encryption handshake during authentication:
|
|
1692
|
-
|
|
1693
|
-
```typescript
|
|
1694
|
-
const ws = new WebSocketServerHelper({
|
|
1695
|
-
// ...
|
|
1696
|
-
requireEncryption: true,
|
|
1697
|
-
handshakeFn: async ({ clientId, userId, data }) => {
|
|
1698
|
-
// Generate ECDH key pair for this client
|
|
1699
|
-
const keyPair = await ecdh.generateKeyPair();
|
|
1700
|
-
// Store privateKey for this client session...
|
|
1701
|
-
return {
|
|
1702
|
-
serverPublicKey: keyPair.publicKeyB64,
|
|
1703
|
-
salt: someSalt,
|
|
1704
|
-
};
|
|
1705
|
-
// Return null/false to reject the handshake
|
|
1706
|
-
},
|
|
1707
|
-
outboundTransformer: async ({ client, event, data }) => {
|
|
1708
|
-
// Transform outbound messages for encrypted clients
|
|
1709
|
-
const encrypted = await encrypt(data, client.metadata.sessionKey);
|
|
1710
|
-
return { event, data: encrypted };
|
|
1711
|
-
// Return null to send unmodified { event, data }
|
|
1712
|
-
},
|
|
1713
|
-
});
|
|
1714
|
-
```
|
|
1715
|
-
|
|
1716
|
-
When a client has encryption enabled:
|
|
1717
|
-
|
|
1718
|
-
- It is unsubscribed from Bun's native pub/sub topics (prevents double delivery)
|
|
1719
|
-
- All messages go through the `outboundTransformer` before `socket.send()`
|
|
1720
|
-
- Room/broadcast messages are delivered individually with concurrency limit (`encryptedBatchLimit`)
|
|
1721
|
-
|
|
1722
|
-
### WebSocket Authentication Flow
|
|
1723
|
-
|
|
1724
|
-
1. Client connects via `server.upgrade()` -- state: `UNAUTHORIZED`
|
|
1725
|
-
2. Auth timeout starts (default 5s)
|
|
1726
|
-
3. Client sends `{ event: 'authenticate', data: { token: '...' } }`
|
|
1727
|
-
4. Server calls `authenticateFn(data)` -- state: `AUTHENTICATING`
|
|
1728
|
-
5. On success:
|
|
1729
|
-
- Client state: `AUTHENTICATED`
|
|
1730
|
-
- Subscribes to broadcast topic
|
|
1731
|
-
- Joins default rooms + own room
|
|
1732
|
-
- If `requireEncryption`: runs `handshakeFn`, enables encryption
|
|
1733
|
-
- Sends `connected` event with `{ id, userId, time, serverPublicKey?, salt? }`
|
|
1734
|
-
- Calls `clientConnectedFn`
|
|
1735
|
-
6. On failure: sends `error` event, closes with code `4003`
|
|
1736
|
-
7. On timeout: closes with code `4001`
|
|
1737
|
-
|
|
1738
|
-
**WebSocket close codes:**
|
|
1739
|
-
|
|
1740
|
-
| Code | Reason |
|
|
1741
|
-
| ------ | ---------------------------------------- |
|
|
1742
|
-
| `1001` | Server shutting down |
|
|
1743
|
-
| `4001` | Authentication timeout |
|
|
1744
|
-
| `4002` | Heartbeat timeout |
|
|
1745
|
-
| `4003` | Authentication failed |
|
|
1746
|
-
| `4004` | Encryption required (handshake failed) |
|
|
1747
|
-
|
|
1748
|
-
**Redis pub/sub channels:**
|
|
1749
|
-
|
|
1750
|
-
| Channel | Purpose |
|
|
1751
|
-
| ------------------ | ---------------------------------------- |
|
|
1752
|
-
| `ws:broadcast` | Broadcast messages across instances |
|
|
1753
|
-
| `ws:room:<name>` | Room-targeted messages |
|
|
1754
|
-
| `ws:client:<id>` | Client-targeted messages |
|
|
1755
|
-
| `ws:user:<id>` | User-targeted messages |
|
|
1756
|
-
|
|
1757
|
-
---
|
|
1758
|
-
|
|
1759
|
-
## Network
|
|
1760
|
-
|
|
1761
|
-
### HTTP Clients
|
|
1762
|
-
|
|
1763
|
-
Two HTTP client implementations sharing a common `IFetchable` interface.
|
|
1764
|
-
|
|
1765
|
-
**Common interface:**
|
|
1766
|
-
|
|
1767
|
-
```typescript
|
|
1768
|
-
interface IFetchable<V, RQ, RS> {
|
|
1769
|
-
send(opts: RQ): Promise<RS>;
|
|
1770
|
-
get(opts: RQ): Promise<RS>;
|
|
1771
|
-
post(opts: RQ): Promise<RS>;
|
|
1772
|
-
put(opts: RQ): Promise<RS>;
|
|
1773
|
-
patch(opts: RQ): Promise<RS>;
|
|
1774
|
-
delete(opts: RQ): Promise<RS>;
|
|
1775
|
-
getWorker(): AxiosInstance | typeof fetch;
|
|
1776
|
-
}
|
|
1777
|
-
```
|
|
1778
|
-
|
|
1779
|
-
**Node Fetch (built-in, zero dependencies):**
|
|
1780
|
-
|
|
1781
|
-
```typescript
|
|
1782
|
-
import { NodeFetchNetworkRequest } from '@venizia/ignis-helpers';
|
|
1783
|
-
|
|
1784
|
-
const api = new NodeFetchNetworkRequest({
|
|
1785
|
-
name: 'github-api',
|
|
1786
|
-
networkOptions: {
|
|
1787
|
-
baseUrl: 'https://api.github.com',
|
|
1788
|
-
headers: { Authorization: 'token ghp_xxx' },
|
|
1789
|
-
// All RequestInit options are supported
|
|
1790
|
-
},
|
|
1791
|
-
});
|
|
1792
|
-
|
|
1793
|
-
const url = api.getRequestUrl({ paths: ['/repos', '/VENIZIA-AI', '/ignis'] });
|
|
1794
|
-
// 'https://api.github.com/repos/VENIZIA-AI/ignis'
|
|
1795
|
-
|
|
1796
|
-
const response = await api.getNetworkService().get({ url, timeout: 5000 });
|
|
1797
|
-
const data = await response.json();
|
|
1798
|
-
|
|
1799
|
-
// With query parameters
|
|
1800
|
-
const searchUrl = api.getRequestUrl({ paths: ['/search', '/repositories'] });
|
|
1801
|
-
const results = await api.getNetworkService().get({
|
|
1802
|
-
url: searchUrl,
|
|
1803
|
-
params: { q: 'ignis', sort: 'stars' },
|
|
1804
|
-
});
|
|
1805
|
-
// Fetches: https://api.github.com/search/repositories?q=ignis&sort=stars
|
|
1806
|
-
|
|
1807
|
-
// POST with body
|
|
1808
|
-
await api.getNetworkService().post({
|
|
1809
|
-
url: api.getRequestUrl({ paths: ['/repos'] }),
|
|
1810
|
-
body: JSON.stringify({ name: 'new-repo' }),
|
|
1811
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1812
|
-
});
|
|
1813
|
-
|
|
1814
|
-
// Default content-type: 'application/json; charset=utf-8'
|
|
1815
|
-
// Timeout implemented via AbortController
|
|
1816
|
-
```
|
|
1817
|
-
|
|
1818
|
-
**Axios (requires `bun add axios`):**
|
|
1819
|
-
|
|
1820
|
-
```typescript
|
|
1821
|
-
import { AxiosNetworkRequest } from '@venizia/ignis-helpers/axios';
|
|
1822
|
-
|
|
1823
|
-
const api = new AxiosNetworkRequest({
|
|
1824
|
-
name: 'payment-api',
|
|
1825
|
-
networkOptions: {
|
|
1826
|
-
baseUrl: 'https://api.payment.com',
|
|
1827
|
-
timeout: 30_000, // Default: 60_000 (60s)
|
|
1828
|
-
headers: { 'X-API-Key': 'key123' },
|
|
1829
|
-
// All AxiosRequestConfig options are supported
|
|
1830
|
-
},
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
const url = api.getRequestUrl({ paths: ['/v1', '/charges'] });
|
|
1834
|
-
const response = await api.getNetworkService().post({
|
|
1835
|
-
url,
|
|
1836
|
-
body: { amount: 1000, currency: 'USD' },
|
|
1837
|
-
});
|
|
1838
|
-
// response.data contains parsed JSON
|
|
1839
|
-
|
|
1840
|
-
// Access the underlying Axios instance for interceptors
|
|
1841
|
-
const axiosInstance = api.getWorker();
|
|
1842
|
-
axiosInstance.interceptors.request.use((config) => {
|
|
1843
|
-
config.headers['X-Request-ID'] = crypto.randomUUID();
|
|
1844
|
-
return config;
|
|
1845
|
-
});
|
|
1846
|
-
axiosInstance.interceptors.response.use(
|
|
1847
|
-
(response) => response,
|
|
1848
|
-
(error) => {
|
|
1849
|
-
if (error.response?.status === 401) {
|
|
1850
|
-
// Handle token refresh
|
|
1851
|
-
}
|
|
1852
|
-
return Promise.reject(error);
|
|
1853
|
-
},
|
|
1854
|
-
);
|
|
1855
|
-
|
|
1856
|
-
// Default configs:
|
|
1857
|
-
// withCredentials: true
|
|
1858
|
-
// validateStatus: (status) => status < 500
|
|
1859
|
-
// HTTPS: auto-creates https.Agent with rejectUnauthorized: false
|
|
1860
|
-
```
|
|
1861
|
-
|
|
1862
|
-
**Differences between implementations:**
|
|
1863
|
-
|
|
1864
|
-
| Feature | NodeFetch | Axios |
|
|
1865
|
-
| ------------- | ----------------------------- | -------------------------------- |
|
|
1866
|
-
| Return type | `Response` (Web API) | `AxiosResponse.data` (parsed) |
|
|
1867
|
-
| Dependencies | None (built-in `fetch`) | `axios` |
|
|
1868
|
-
| Timeout | `AbortController` | Axios built-in |
|
|
1869
|
-
| Interceptors | Manual | Built-in request/response |
|
|
1870
|
-
| HTTPS | Standard | Auto `https.Agent` |
|
|
1871
|
-
| Credentials | Manual headers | `withCredentials: true` |
|
|
1872
|
-
|
|
1873
|
-
### TCP
|
|
1874
|
-
|
|
1875
|
-
TCP server and client with optional TLS, authentication flow, and client state tracking.
|
|
1876
|
-
|
|
1877
|
-
**Server:**
|
|
1878
|
-
|
|
1879
|
-
```typescript
|
|
1880
|
-
import { NetworkTcpServer, NetworkTcpClient } from '@venizia/ignis-helpers';
|
|
1881
|
-
|
|
1882
|
-
const server = new NetworkTcpServer({
|
|
1883
|
-
identifier: 'data-server',
|
|
1884
|
-
serverOptions: {}, // net.ServerOpts
|
|
1885
|
-
listenOptions: { host: '0.0.0.0', port: 9000 }, // net.ListenOptions
|
|
1886
|
-
authenticateOptions: {
|
|
1887
|
-
required: true, // Require client auth
|
|
1888
|
-
duration: 5000, // 5s to authenticate
|
|
1889
|
-
},
|
|
1890
|
-
extraEvents: {
|
|
1891
|
-
'timeout': ({ id, socket }) => {
|
|
1892
|
-
console.log(`Client ${id} timed out`);
|
|
1893
|
-
},
|
|
1894
|
-
},
|
|
1895
|
-
onServerReady: ({ server }) => {
|
|
1896
|
-
console.log('Server listening on', server.address());
|
|
1897
|
-
},
|
|
1898
|
-
onClientConnected: ({ id, socket }) => {
|
|
1899
|
-
console.log(`Client ${id} connected from ${socket.remoteAddress}:${socket.remotePort}`);
|
|
1900
|
-
// Client starts as 'unauthorized' when authenticateOptions.required = true
|
|
1901
|
-
},
|
|
1902
|
-
onClientData: ({ id, data }) => {
|
|
1903
|
-
console.log(`[${id}] Data:`, data.toString());
|
|
1904
|
-
},
|
|
1905
|
-
onClientClose: ({ id }) => {
|
|
1906
|
-
console.log(`Client ${id} disconnected`);
|
|
1907
|
-
},
|
|
1908
|
-
onClientError: ({ id, error }) => {
|
|
1909
|
-
console.error(`Client ${id} error:`, error);
|
|
1910
|
-
},
|
|
1911
|
-
});
|
|
1912
|
-
|
|
1913
|
-
// Authentication management
|
|
1914
|
-
server.doAuthenticate({ id: clientId, state: 'authenticated' });
|
|
1915
|
-
// States: 'unauthorized' | 'authenticating' | 'authenticated'
|
|
1916
|
-
|
|
1917
|
-
// Send data to a client
|
|
1918
|
-
server.emit({ clientId: id, payload: 'Hello client!' });
|
|
1919
|
-
server.emit({ clientId: id, payload: Buffer.from([0x01, 0x02]) });
|
|
1920
|
-
|
|
1921
|
-
// Get connected clients
|
|
1922
|
-
const clients = server.getClients(); // Record<string, ITcpSocketClient>
|
|
1923
|
-
const client = server.getClient({ id: clientId });
|
|
1924
|
-
// { id, socket, state, subscriptions: Set, storage: { connectedAt, authenticatedAt, ... } }
|
|
1925
|
-
```
|
|
1926
|
-
|
|
1927
|
-
**Client:**
|
|
1928
|
-
|
|
1929
|
-
```typescript
|
|
1930
|
-
const client = new NetworkTcpClient({
|
|
1931
|
-
identifier: 'data-client',
|
|
1932
|
-
options: { host: '127.0.0.1', port: 9000 }, // net.TcpSocketConnectOpts
|
|
1933
|
-
reconnect: true, // Auto-reconnect on error (default: false)
|
|
1934
|
-
maxRetry: 5, // Max reconnection attempts (default: 5)
|
|
1935
|
-
encoding: 'utf-8', // Socket encoding (optional)
|
|
1936
|
-
onConnected: ({ client }) => {
|
|
1937
|
-
console.log('Connected to server');
|
|
1938
|
-
},
|
|
1939
|
-
onData: ({ identifier, message }) => {
|
|
1940
|
-
console.log(`[${identifier}] Received:`, message);
|
|
1941
|
-
},
|
|
1942
|
-
onClosed: ({ client }) => {
|
|
1943
|
-
console.log('Connection closed');
|
|
1944
|
-
},
|
|
1945
|
-
onError: (error) => {
|
|
1946
|
-
console.error('Error:', error);
|
|
1947
|
-
// Auto-reconnect fires after 5000ms if reconnect: true
|
|
1948
|
-
},
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
client.connect({ resetReconnectCounter: true });
|
|
1952
|
-
client.emit({ payload: 'Hello server!' });
|
|
1953
|
-
client.emit({ payload: Buffer.from([0x01, 0x02]) });
|
|
1954
|
-
|
|
1955
|
-
client.isConnected(); // boolean
|
|
1956
|
-
client.forceReconnect(); // Destroy + reconnect with counter reset
|
|
1957
|
-
client.disconnect(); // Destroy + clear reconnect timer
|
|
1958
|
-
```
|
|
1959
|
-
|
|
1960
|
-
**TLS variants:** `NetworkTLSTcpServer` and `NetworkTLSTcpClient` accept `tls.TLSSocketOptions` and `tls.ConnectionOptions` respectively. They use the same API but with encrypted connections.
|
|
1961
|
-
|
|
1962
|
-
### UDP
|
|
1963
|
-
|
|
1964
|
-
```typescript
|
|
1965
|
-
import { NetworkUdpClient } from '@venizia/ignis-helpers';
|
|
1966
|
-
|
|
1967
|
-
// Unicast
|
|
1968
|
-
const udpSimple = NetworkUdpClient.newInstance({
|
|
1969
|
-
identifier: 'udp-sender',
|
|
1970
|
-
port: 5000,
|
|
1971
|
-
host: '0.0.0.0',
|
|
1972
|
-
onData: ({ message, remoteInfo }) => {
|
|
1973
|
-
console.log(`From ${remoteInfo.address}:${remoteInfo.port}:`, message.toString());
|
|
1974
|
-
},
|
|
1975
|
-
});
|
|
1976
|
-
udpSimple.connect();
|
|
1977
|
-
|
|
1978
|
-
// Multicast
|
|
1979
|
-
const udp = NetworkUdpClient.newInstance({
|
|
1980
|
-
identifier: 'sensor-listener',
|
|
1981
|
-
port: 5000,
|
|
1982
|
-
reuseAddr: true,
|
|
1983
|
-
multicastAddress: {
|
|
1984
|
-
groups: ['239.1.2.3', '239.1.2.4'],
|
|
1985
|
-
interface: '0.0.0.0',
|
|
1986
|
-
},
|
|
1987
|
-
onData: ({ message, remoteInfo }) => {
|
|
1988
|
-
console.log(`From ${remoteInfo.address}:${remoteInfo.port}:`, message.toString());
|
|
1989
|
-
},
|
|
1990
|
-
onBind: async ({ socket, multicastAddress }) => {
|
|
1991
|
-
// Join multicast groups after binding
|
|
1992
|
-
for (const group of multicastAddress?.groups ?? []) {
|
|
1993
|
-
socket.addMembership(group, multicastAddress?.interface);
|
|
1994
|
-
}
|
|
1995
|
-
},
|
|
1996
|
-
onConnected: ({ identifier, host, port }) => {
|
|
1997
|
-
console.log(`${identifier} listening on ${host}:${port}`);
|
|
1998
|
-
},
|
|
1999
|
-
onClosed: ({ identifier }) => {
|
|
2000
|
-
console.log(`${identifier} closed`);
|
|
2001
|
-
},
|
|
2002
|
-
onError: ({ identifier, error }) => {
|
|
2003
|
-
console.error(`${identifier} error:`, error);
|
|
2004
|
-
},
|
|
2005
|
-
});
|
|
2006
|
-
|
|
2007
|
-
udp.connect();
|
|
2008
|
-
udp.isConnected(); // boolean
|
|
2009
|
-
udp.disconnect();
|
|
2010
|
-
```
|
|
2011
|
-
|
|
2012
|
-
---
|
|
2013
|
-
|
|
2014
|
-
## UID (Snowflake)
|
|
2015
|
-
|
|
2016
|
-
Snowflake ID generator producing unique, time-sortable IDs with Base62 encoding.
|
|
2017
|
-
|
|
2018
|
-
### Bit Layout
|
|
2019
|
-
|
|
2020
|
-
```
|
|
2021
|
-
70-bit Snowflake ID
|
|
2022
|
-
+-----------------+----------+----------+
|
|
2023
|
-
| Timestamp | Worker | Sequence |
|
|
2024
|
-
| (48 bits) | (10 bits)| (12 bits)|
|
|
2025
|
-
+-----------------+----------+----------+
|
|
2026
|
-
MSB LSB
|
|
2027
|
-
```
|
|
2028
|
-
|
|
2029
|
-
| Field | Bits | Range | Description |
|
|
2030
|
-
| --------- | ---- | ----------------------------- | ------------------------------ |
|
|
2031
|
-
| Timestamp | 48 | ~8,919 years from epoch | Milliseconds since epoch |
|
|
2032
|
-
| Worker ID | 10 | 0--1023 (1024 workers) | Unique worker identifier |
|
|
2033
|
-
| Sequence | 12 | 0--4095 per millisecond | Per-millisecond counter |
|
|
2034
|
-
|
|
2035
|
-
**Output:** Base62 encoded string (10--12 characters).
|
|
2036
|
-
**Default epoch:** 2025-01-01 00:00:00 UTC (`1735689600000`).
|
|
2037
|
-
**Lifespan:** Until approximately 10,944 AD.
|
|
2038
|
-
|
|
2039
|
-
### UID API Reference
|
|
2040
|
-
|
|
2041
|
-
```typescript
|
|
2042
|
-
import { SnowflakeUidHelper, SnowflakeConfig } from '@venizia/ignis-helpers';
|
|
2043
|
-
|
|
2044
|
-
// Default: workerId=199, epoch=2025-01-01
|
|
2045
|
-
const uid = new SnowflakeUidHelper();
|
|
2046
|
-
|
|
2047
|
-
// Custom configuration
|
|
2048
|
-
const uid = new SnowflakeUidHelper({
|
|
2049
|
-
workerId: 42, // 0-1023
|
|
2050
|
-
epoch: BigInt(1735689600000), // Custom epoch
|
|
2051
|
-
});
|
|
2052
|
-
|
|
2053
|
-
// --- Generate IDs ---
|
|
2054
|
-
|
|
2055
|
-
const id = uid.nextId(); // Base62 string, e.g., "9du1sJXO88"
|
|
2056
|
-
const snowflake = uid.nextSnowflake(); // Raw BigInt, e.g., 130546360012247045n
|
|
2057
|
-
|
|
2058
|
-
// --- Parse an ID back to components ---
|
|
2059
|
-
|
|
2060
|
-
const parsed = uid.parseId('9du1sJXO88');
|
|
2061
|
-
// {
|
|
2062
|
-
// raw: 130546360012247045n,
|
|
2063
|
-
// timestamp: Date, // When the ID was generated
|
|
2064
|
-
// workerId: 199, // Which worker generated it
|
|
2065
|
-
// sequence: 0, // Sequence within that millisecond
|
|
2066
|
-
// }
|
|
2067
|
-
|
|
2068
|
-
// --- Extract individual components from raw snowflake ---
|
|
2069
|
-
|
|
2070
|
-
const ts = uid.extractTimestamp(snowflake); // Date
|
|
2071
|
-
const wid = uid.extractWorkerId(snowflake); // number (0-1023)
|
|
2072
|
-
const seq = uid.extractSequence(snowflake); // number (0-4095)
|
|
2073
|
-
|
|
2074
|
-
// --- Base62 encoding/decoding ---
|
|
2075
|
-
|
|
2076
|
-
const encoded = uid.encodeBase62(BigInt(123456789)); // "8m0Kx"
|
|
2077
|
-
const decoded = uid.decodeBase62('8m0Kx'); // 123456789n
|
|
2078
|
-
|
|
2079
|
-
// --- Configuration constants ---
|
|
2080
|
-
|
|
2081
|
-
SnowflakeConfig.DEFAULT_EPOCH; // 1735689600000n (2025-01-01)
|
|
2082
|
-
SnowflakeConfig.MAX_WORKER_ID; // 1023n
|
|
2083
|
-
SnowflakeConfig.MAX_SEQUENCE; // 4095n
|
|
2084
|
-
SnowflakeConfig.MAX_CLOCK_BACKWARD_MS; // 100n (tolerance for clock drift)
|
|
2085
|
-
|
|
2086
|
-
// Get current worker ID
|
|
2087
|
-
uid.getWorkerId(); // number
|
|
2088
|
-
```
|
|
2089
|
-
|
|
2090
|
-
### Throughput and Limits
|
|
2091
|
-
|
|
2092
|
-
| Metric | Value |
|
|
2093
|
-
| ---------------------------------------- | -------------------------------- |
|
|
2094
|
-
| **IDs per millisecond per worker** | 4,096 |
|
|
2095
|
-
| **IDs per second per worker** | 4,096,000 |
|
|
2096
|
-
| **Max workers** | 1,024 |
|
|
2097
|
-
| **Total IDs per second (all workers)** | ~4.2 billion |
|
|
2098
|
-
| **ID lifespan** | ~8,919 years from epoch |
|
|
2099
|
-
| **Clock backward tolerance** | 100ms (waits), >100ms throws |
|
|
2100
|
-
| **Base62 output length** | 10--12 characters |
|
|
2101
|
-
| **Base62 charset** | `0-9A-Za-z` |
|
|
2102
|
-
|
|
2103
|
-
**Clock drift handling:**
|
|
2104
|
-
|
|
2105
|
-
- If clock moves backward <= 100ms: waits (spin-loop) until clock catches up
|
|
2106
|
-
- If clock moves backward > 100ms: throws `ApplicationError` (500)
|
|
2107
|
-
- If sequence exhausted in a millisecond (>4095): waits for next millisecond
|
|
2108
|
-
|
|
2109
|
-
**Expiry warning:** The generator logs a warning when approaching the 48-bit timestamp limit (~10 years before expiry). Action: plan migration to a new epoch.
|
|
2110
|
-
|
|
2111
|
-
---
|
|
2112
|
-
|
|
2113
|
-
## Environment
|
|
2114
|
-
|
|
2115
|
-
### Environment Detection
|
|
2116
|
-
|
|
2117
|
-
```typescript
|
|
2118
|
-
import { Environment } from '@venizia/ignis-helpers';
|
|
2119
|
-
|
|
2120
|
-
const env = Environment.current; // process.env.NODE_ENV or 'development'
|
|
2121
|
-
const isProd = Environment.is({ name: 'production' });
|
|
2122
|
-
|
|
2123
|
-
// Available constants
|
|
2124
|
-
Environment.LOCAL; // 'local'
|
|
2125
|
-
Environment.DEBUG; // 'debug'
|
|
2126
|
-
Environment.DEVELOPMENT; // 'development'
|
|
2127
|
-
Environment.ALPHA; // 'alpha'
|
|
2128
|
-
Environment.BETA; // 'beta'
|
|
2129
|
-
Environment.STAGING; // 'staging'
|
|
2130
|
-
Environment.PRODUCTION; // 'production'
|
|
2131
|
-
|
|
2132
|
-
// Common environment set (used for debug level gating)
|
|
2133
|
-
Environment.COMMON_ENVS; // Set containing all above values
|
|
2134
|
-
```
|
|
2135
|
-
|
|
2136
|
-
### Application Environment
|
|
2137
|
-
|
|
2138
|
-
Prefixed environment variable management.
|
|
2139
|
-
|
|
2140
|
-
```typescript
|
|
2141
|
-
import { applicationEnvironment, ApplicationEnvironment } from '@venizia/ignis-helpers';
|
|
2142
|
-
|
|
2143
|
-
// Default instance uses prefix 'APP_ENV' (configurable via APPLICATION_ENV_PREFIX env var)
|
|
2144
|
-
const dbHost = applicationEnvironment.get<string>('APP_ENV_DB_HOST');
|
|
2145
|
-
applicationEnvironment.set('APP_ENV_FEATURE_FLAG', true);
|
|
2146
|
-
|
|
2147
|
-
const isDev = applicationEnvironment.isDevelopment();
|
|
2148
|
-
const keys = applicationEnvironment.keys();
|
|
2149
|
-
// Only returns keys starting with the configured prefix
|
|
2150
|
-
|
|
2151
|
-
// Custom prefix
|
|
2152
|
-
const myEnv = new ApplicationEnvironment({
|
|
2153
|
-
prefix: 'MY_APP',
|
|
2154
|
-
envs: process.env,
|
|
2155
|
-
});
|
|
2156
|
-
const val = myEnv.get<string>('MY_APP_SECRET');
|
|
2157
|
-
```
|
|
2158
|
-
|
|
2159
|
-
### Runtime Detection
|
|
2160
|
-
|
|
2161
|
-
```typescript
|
|
2162
|
-
import { RuntimeModules } from '@venizia/ignis-helpers';
|
|
2163
|
-
|
|
2164
|
-
const runtime = RuntimeModules.detect(); // 'bun' or 'node'
|
|
2165
|
-
const isBun = RuntimeModules.isBun(); // Checks: typeof Bun !== 'undefined'
|
|
2166
|
-
const isNode = RuntimeModules.isNode();
|
|
2167
|
-
```
|
|
2168
|
-
|
|
2169
|
-
---
|
|
2170
|
-
|
|
2171
|
-
## Error
|
|
2172
|
-
|
|
2173
|
-
Standardized error class with HTTP status codes and optional message codes.
|
|
2174
|
-
|
|
2175
|
-
```typescript
|
|
2176
|
-
import { ApplicationError, getError } from '@venizia/ignis-helpers';
|
|
2177
|
-
|
|
2178
|
-
// Using factory function (preferred)
|
|
2179
|
-
throw getError({
|
|
2180
|
-
statusCode: 404,
|
|
2181
|
-
message: 'User not found',
|
|
2182
|
-
messageCode: 'user.not_found', // Optional machine-readable code
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
// Using class constructor
|
|
2186
|
-
throw new ApplicationError({
|
|
2187
|
-
statusCode: 400,
|
|
2188
|
-
message: 'Invalid input',
|
|
2189
|
-
});
|
|
2190
|
-
|
|
2191
|
-
// Static factory
|
|
2192
|
-
throw ApplicationError.getError({
|
|
2193
|
-
statusCode: 500,
|
|
2194
|
-
message: 'Internal error',
|
|
2195
|
-
});
|
|
2196
|
-
|
|
2197
|
-
// ApplicationError extends Error
|
|
2198
|
-
try {
|
|
2199
|
-
throw getError({ statusCode: 403, message: 'Forbidden', messageCode: 'auth.forbidden' });
|
|
2200
|
-
} catch (error) {
|
|
2201
|
-
if (error instanceof ApplicationError) {
|
|
2202
|
-
console.log(error.statusCode); // 403
|
|
2203
|
-
console.log(error.message); // 'Forbidden'
|
|
2204
|
-
console.log(error.messageCode); // 'auth.forbidden'
|
|
2205
|
-
console.log(error.stack); // Standard Error stack trace
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
```
|
|
2209
|
-
|
|
2210
|
-
**Error Zod schema (for OpenAPI):**
|
|
2211
|
-
|
|
2212
|
-
```typescript
|
|
2213
|
-
import { ErrorSchema } from '@venizia/ignis-helpers';
|
|
2214
|
-
|
|
2215
|
-
// ErrorSchema is a Zod schema used in OpenAPI route definitions
|
|
2216
|
-
// { name?: string, statusCode?: number, messageCode?: string, message: string }
|
|
2217
|
-
```
|
|
2218
|
-
|
|
2219
|
-
---
|
|
2220
|
-
|
|
2221
|
-
## Worker Thread
|
|
2222
|
-
|
|
2223
|
-
Worker pool management and inter-thread messaging.
|
|
2224
|
-
|
|
2225
|
-
### Worker Pool
|
|
2226
|
-
|
|
2227
|
-
Singleton pool that tracks worker threads up to the number of CPU cores.
|
|
2228
|
-
|
|
2229
|
-
```typescript
|
|
2230
|
-
import { WorkerPoolHelper, BaseWorkerHelper } from '@venizia/ignis-helpers';
|
|
2231
|
-
|
|
2232
|
-
const pool = WorkerPoolHelper.getInstance(); // Singleton
|
|
2233
|
-
|
|
2234
|
-
// Create and register a worker
|
|
2235
|
-
const worker = new BaseWorkerHelper({
|
|
2236
|
-
identifier: 'image-processor',
|
|
2237
|
-
path: './workers/image-processor.ts',
|
|
2238
|
-
options: { workerData: { quality: 80 } }, // WorkerOptions
|
|
2239
|
-
eventHandlers: {
|
|
2240
|
-
onOnline: () => console.log('Worker online'),
|
|
2241
|
-
onMessage: ({ message }) => console.log('Result:', message),
|
|
2242
|
-
onError: ({ error }) => console.error('Worker error:', error),
|
|
2243
|
-
onExit: ({ code }) => console.log('Worker exited:', code),
|
|
2244
|
-
onMessageError: ({ error }) => console.error('Message error:', error),
|
|
2245
|
-
},
|
|
2246
|
-
});
|
|
2247
|
-
|
|
2248
|
-
pool.register({ key: 'image-processor', worker });
|
|
2249
|
-
|
|
2250
|
-
// Check pool
|
|
2251
|
-
pool.has({ key: 'image-processor' }); // true
|
|
2252
|
-
pool.size(); // 1
|
|
2253
|
-
pool.get({ key: 'image-processor' }); // BaseWorkerHelper instance
|
|
2254
|
-
|
|
2255
|
-
// Unregister (terminates worker thread)
|
|
2256
|
-
await pool.unregister({ key: 'image-processor' });
|
|
2257
|
-
|
|
2258
|
-
// Pool warns when size reaches CPU core count (configurable via ignoreMaxWarning)
|
|
2259
|
-
```
|
|
2260
|
-
|
|
2261
|
-
### Worker Bus
|
|
2262
|
-
|
|
2263
|
-
Message bus for structured inter-thread communication using `MessagePort`.
|
|
2264
|
-
|
|
2265
|
-
```typescript
|
|
2266
|
-
import {
|
|
2267
|
-
BaseWorkerBusHelper,
|
|
2268
|
-
BaseWorkerMessageBusHandlerHelper,
|
|
2269
|
-
BaseWorkerThreadHelper,
|
|
2270
|
-
} from '@venizia/ignis-helpers';
|
|
2271
|
-
|
|
2272
|
-
// --- Inside worker thread ---
|
|
2273
|
-
const thread = new BaseWorkerThreadHelper({ scope: 'ImageWorker' });
|
|
2274
|
-
// Throws if called from main thread
|
|
2275
|
-
|
|
2276
|
-
const busHandler = new BaseWorkerMessageBusHandlerHelper({
|
|
2277
|
-
scope: 'image-bus-handler',
|
|
2278
|
-
onMessage: ({ message }) => {
|
|
2279
|
-
console.log('Received from main:', message);
|
|
2280
|
-
},
|
|
2281
|
-
onClose: () => console.log('Bus closed'),
|
|
2282
|
-
onError: ({ error }) => console.error('Bus error:', error),
|
|
2283
|
-
onExit: ({ exitCode }) => console.log('Bus exited:', exitCode),
|
|
2284
|
-
});
|
|
2285
|
-
|
|
2286
|
-
const bus = new BaseWorkerBusHelper({
|
|
2287
|
-
scope: 'image-bus',
|
|
2288
|
-
port: parentPort!, // MessagePort from worker_threads
|
|
2289
|
-
busHandler,
|
|
2290
|
-
});
|
|
2291
|
-
|
|
2292
|
-
thread.bindWorkerBus({ key: 'main', bus });
|
|
2293
|
-
|
|
2294
|
-
// Send message to main thread
|
|
2295
|
-
bus.postMessage({
|
|
2296
|
-
message: { type: 'result', data: processedImage },
|
|
2297
|
-
transferList: undefined, // Optional Transferable[]
|
|
2298
|
-
});
|
|
2299
|
-
|
|
2300
|
-
// Lifecycle hooks
|
|
2301
|
-
// bus.onBeforePostMessage = ({ message }) => { ... };
|
|
2302
|
-
// bus.onAfterPostMessage = ({ message }) => { ... };
|
|
2303
|
-
|
|
2304
|
-
// Retrieve a bound bus
|
|
2305
|
-
const mainBus = thread.getWorkerBus({ key: 'main' });
|
|
2306
|
-
|
|
2307
|
-
// Unbind (removes all listeners)
|
|
2308
|
-
thread.unbindWorkerBus({ key: 'main' });
|
|
2309
|
-
```
|
|
2310
|
-
|
|
2311
|
-
---
|
|
2312
|
-
|
|
2313
|
-
## Common Utilities
|
|
2314
|
-
|
|
2315
|
-
### Date Utility
|
|
2316
|
-
|
|
2317
|
-
Day.js with UTC, timezone, weekday, isoWeek, and custom parse format plugins pre-configured.
|
|
2318
|
-
|
|
2319
|
-
```typescript
|
|
2320
|
-
import {
|
|
2321
|
-
dayjs,
|
|
2322
|
-
sleep,
|
|
2323
|
-
isWeekday,
|
|
2324
|
-
getPreviousWeekday,
|
|
2325
|
-
getNextWeekday,
|
|
2326
|
-
getDateTz,
|
|
2327
|
-
hrTime,
|
|
2328
|
-
} from '@venizia/ignis-helpers';
|
|
2329
|
-
|
|
2330
|
-
// Default timezone: Asia/Ho_Chi_Minh (configurable via APP_ENV_APPLICATION_TIMEZONE)
|
|
2331
|
-
const now = dayjs();
|
|
2332
|
-
const utc = dayjs.utc();
|
|
2333
|
-
|
|
2334
|
-
// Sleep utility
|
|
2335
|
-
await sleep(1000); // Sleep 1 second
|
|
2336
|
-
|
|
2337
|
-
// Weekday checks (Monday=1 through Friday=5)
|
|
2338
|
-
isWeekday('2025-01-06'); // true (Monday)
|
|
2339
|
-
isWeekday('2025-01-05'); // false (Sunday)
|
|
2340
|
-
|
|
2341
|
-
// Navigate to adjacent weekdays
|
|
2342
|
-
const prevWeekday = getPreviousWeekday(); // Previous weekday from today
|
|
2343
|
-
const nextWeekday = getNextWeekday(); // Next weekday from today
|
|
2344
|
-
const prevFromDate = getPreviousWeekday({ date: '2025-01-06' });
|
|
2345
|
-
|
|
2346
|
-
// Timezone conversion
|
|
2347
|
-
const tzDate = getDateTz({
|
|
2348
|
-
date: '2025-01-01T00:00:00Z',
|
|
2349
|
-
timezone: 'America/New_York',
|
|
2350
|
-
useClientTz: false, // default
|
|
2351
|
-
timeOffset: -5, // Additional hour offset
|
|
2352
|
-
});
|
|
2353
|
-
|
|
2354
|
-
// High-resolution time (seconds with 9 decimal places)
|
|
2355
|
-
const elapsed = hrTime(); // e.g., 1234567.890123456
|
|
2356
|
-
```
|
|
2357
|
-
|
|
2358
|
-
### Parse Utility
|
|
2359
|
-
|
|
2360
|
-
```typescript
|
|
2361
|
-
import {
|
|
2362
|
-
int, float, isInt, isFloat, toBoolean,
|
|
2363
|
-
toCamel, keysToCamel,
|
|
2364
|
-
toStringDecimal, getNumberValue, getUID,
|
|
2365
|
-
parseArrayToRecordWithKey, parseArrayToMapWithKey,
|
|
2366
|
-
} from '@venizia/ignis-helpers';
|
|
2367
|
-
|
|
2368
|
-
// --- Numeric parsing ---
|
|
2369
|
-
int('42'); // 42
|
|
2370
|
-
int('3.14'); // 3
|
|
2371
|
-
int('1,234'); // 1234 (removes commas)
|
|
2372
|
-
int(null); // 0
|
|
2373
|
-
int(NaN); // 0
|
|
2374
|
-
|
|
2375
|
-
float('3.14159', 2); // 3.14
|
|
2376
|
-
float('3.14159', 4); // 3.1416 (rounds using lodash.round)
|
|
2377
|
-
float('1,234.56'); // 1234.56
|
|
2378
|
-
|
|
2379
|
-
isInt(42); // true
|
|
2380
|
-
isInt(3.14); // false
|
|
2381
|
-
isFloat(3.14); // true
|
|
2382
|
-
isFloat(42); // true (integers are also valid floats)
|
|
2383
|
-
|
|
2384
|
-
// --- Boolean parsing ---
|
|
2385
|
-
toBoolean('true'); // true
|
|
2386
|
-
toBoolean(''); // false
|
|
2387
|
-
toBoolean('false'); // false
|
|
2388
|
-
toBoolean('0'); // false
|
|
2389
|
-
toBoolean(0); // false
|
|
2390
|
-
toBoolean(null); // false
|
|
2391
|
-
toBoolean(undefined); // false
|
|
2392
|
-
toBoolean(1); // true
|
|
2393
|
-
toBoolean('yes'); // true (any non-falsy string)
|
|
2394
|
-
|
|
2395
|
-
// --- Case conversion ---
|
|
2396
|
-
toCamel('snake_case'); // 'snakeCase'
|
|
2397
|
-
toCamel('kebab-case'); // 'kebabCase'
|
|
2398
|
-
toCamel('already_camelCase'); // 'alreadyCamelCase'
|
|
2399
|
-
|
|
2400
|
-
keysToCamel({ user_name: 'Alice', home_address: { zip_code: '12345' } });
|
|
2401
|
-
// { userName: 'Alice', homeAddress: { zipCode: '12345' } }
|
|
2402
|
-
// Recursively converts nested objects (but not arrays or Dates)
|
|
2403
|
-
|
|
2404
|
-
// --- Number formatting ---
|
|
2405
|
-
toStringDecimal(1234567.891, 2); // '1,234,567.89' (en-US locale)
|
|
2406
|
-
toStringDecimal(1234567, 0); // '1,234,567'
|
|
2407
|
-
toStringDecimal(3.14159, 4); // '3.1416'
|
|
2408
|
-
toStringDecimal(42, 2, { useLocaleFormat: false }); // '42.00'
|
|
2409
|
-
|
|
2410
|
-
// EU number format support
|
|
2411
|
-
getNumberValue('1.234,56', { locale: 'eu' }); // 1234 (parsed as int)
|
|
2412
|
-
getNumberValue('1.234,56', { locale: 'eu', method: 'float' }); // 1234.56
|
|
2413
|
-
getNumberValue('1,234.56', { locale: 'us' }); // 1234 (default locale)
|
|
2414
|
-
|
|
2415
|
-
// --- Random ID ---
|
|
2416
|
-
getUID(); // Random string, e.g., 'K7X2F9ZA1B' (Math.random base36)
|
|
2417
|
-
|
|
2418
|
-
// --- Array to keyed collections ---
|
|
2419
|
-
const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
|
|
2420
|
-
|
|
2421
|
-
const record = parseArrayToRecordWithKey({ arr: users, keyMap: 'id' });
|
|
2422
|
-
// { 1: { id: 1, name: 'A' }, 2: { id: 2, name: 'B' } }
|
|
2423
|
-
|
|
2424
|
-
const map = parseArrayToMapWithKey(users, 'id');
|
|
2425
|
-
// Map { 1 => { id: 1, name: 'A' }, 2 => { id: 2, name: 'B' } }
|
|
2426
|
-
// Note: Duplicate keys = last element wins in both cases
|
|
2427
|
-
```
|
|
2428
|
-
|
|
2429
|
-
### Promise Utility
|
|
2430
|
-
|
|
2431
|
-
```typescript
|
|
2432
|
-
import {
|
|
2433
|
-
executePromiseWithLimit,
|
|
2434
|
-
isPromiseLike,
|
|
2435
|
-
getDeepProperty,
|
|
2436
|
-
transformValueOrPromise,
|
|
2437
|
-
} from '@venizia/ignis-helpers';
|
|
2438
|
-
|
|
2439
|
-
// --- Parallel task execution with concurrency limit ---
|
|
2440
|
-
// Execute 100 tasks with max 10 concurrent
|
|
2441
|
-
const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/item/${i}`);
|
|
2442
|
-
const tasks = urls.map(url => () => fetch(url).then(r => r.json()));
|
|
2443
|
-
|
|
2444
|
-
const results = await executePromiseWithLimit({
|
|
2445
|
-
tasks, // Array<() => Promise<T>>
|
|
2446
|
-
limit: 10, // Max concurrent promises
|
|
2447
|
-
onTaskDone: ({ result }) => {
|
|
2448
|
-
console.log('Task completed:', result);
|
|
2449
|
-
},
|
|
2450
|
-
});
|
|
2451
|
-
// results: all 100 resolved values in order
|
|
2452
|
-
|
|
2453
|
-
// --- Promise detection ---
|
|
2454
|
-
isPromiseLike(Promise.resolve(1)); // true
|
|
2455
|
-
isPromiseLike({ then: () => {} }); // true (thenable)
|
|
2456
|
-
isPromiseLike(42); // false
|
|
2457
|
-
isPromiseLike(null); // false
|
|
2458
|
-
|
|
2459
|
-
// --- Deep property access ---
|
|
2460
|
-
const obj = { a: { b: { c: 42 } } };
|
|
2461
|
-
const value = getDeepProperty(obj, 'a.b.c'); // 42
|
|
2462
|
-
// Throws ApplicationError if any part of the path is null/undefined
|
|
2463
|
-
|
|
2464
|
-
// --- Transform value or promise ---
|
|
2465
|
-
const transformed = await transformValueOrPromise(
|
|
2466
|
-
fetchData(), // ValueOrPromise<T>
|
|
2467
|
-
(data) => data.map(item => item.name), // Transformer function
|
|
2468
|
-
);
|
|
2469
|
-
// Works with both sync values and promises
|
|
2470
|
-
```
|
|
2471
|
-
|
|
2472
|
-
### Request Utility
|
|
2473
|
-
|
|
2474
|
-
```typescript
|
|
2475
|
-
import {
|
|
2476
|
-
parseMultipartBody,
|
|
2477
|
-
sanitizeFilename,
|
|
2478
|
-
encodeRFC5987,
|
|
2479
|
-
createContentDispositionHeader,
|
|
2480
|
-
} from '@venizia/ignis-helpers';
|
|
2481
|
-
|
|
2482
|
-
// --- Parse multipart form data (Hono context) ---
|
|
2483
|
-
|
|
2484
|
-
// Memory storage (default) -- files kept as Buffer
|
|
2485
|
-
const files = await parseMultipartBody({
|
|
2486
|
-
context: c, // Hono Context (or anything with .req.formData())
|
|
2487
|
-
storage: 'memory', // 'memory' | 'disk'
|
|
2488
|
-
});
|
|
2489
|
-
// Returns: IParsedFile[]
|
|
2490
|
-
// [{ fieldname, originalname, encoding, mimetype, size, buffer }]
|
|
2491
|
-
|
|
2492
|
-
// Disk storage -- files written to filesystem
|
|
2493
|
-
const diskFiles = await parseMultipartBody({
|
|
2494
|
-
context: c,
|
|
2495
|
-
storage: 'disk',
|
|
2496
|
-
uploadDir: './uploads', // Default: './uploads' (created if missing)
|
|
2497
|
-
});
|
|
2498
|
-
// Returns: IParsedFile[]
|
|
2499
|
-
// [{ fieldname, originalname, encoding, mimetype, size, filename, path }]
|
|
2500
|
-
// filename format: '<timestamp>-<random>-<sanitized-original>'
|
|
2501
|
-
|
|
2502
|
-
// --- Filename sanitization ---
|
|
2503
|
-
sanitizeFilename('photo.jpg'); // 'photo.jpg'
|
|
2504
|
-
sanitizeFilename('../../../etc/passwd'); // '______etc_passwd' or 'download'
|
|
2505
|
-
sanitizeFilename('.hidden'); // 'hidden' (leading dot removed)
|
|
2506
|
-
sanitizeFilename('file with spaces.txt'); // 'file with spaces.txt'
|
|
2507
|
-
sanitizeFilename(''); // 'download' (fallback)
|
|
2508
|
-
sanitizeFilename('...'); // 'download' (fallback)
|
|
2509
|
-
// Allowed characters: alphanumeric, spaces, hyphens, underscores, dots
|
|
2510
|
-
|
|
2511
|
-
// --- RFC 5987 encoding ---
|
|
2512
|
-
encodeRFC5987("report (2025).pdf");
|
|
2513
|
-
// 'report%20%282025%29.pdf'
|
|
2514
|
-
|
|
2515
|
-
// --- Content-Disposition header (RFC 5987 compliant) ---
|
|
2516
|
-
const header = createContentDispositionHeader({
|
|
2517
|
-
filename: 'report (2025).pdf',
|
|
2518
|
-
type: 'attachment', // 'attachment' | 'inline'
|
|
2519
|
-
});
|
|
2520
|
-
// 'attachment; filename="report__2025_.pdf"; filename*=UTF-8\'\'report__2025_.pdf'
|
|
2521
|
-
// Includes both ASCII fallback (filename=) and UTF-8 encoded (filename*=) for compatibility
|
|
2522
|
-
```
|
|
2523
|
-
|
|
2524
|
-
### Performance Utility
|
|
2525
|
-
|
|
2526
|
-
```typescript
|
|
2527
|
-
import {
|
|
2528
|
-
getPerformanceCheckpoint,
|
|
2529
|
-
getExecutedPerformance,
|
|
2530
|
-
executeWithPerformanceMeasure,
|
|
2531
|
-
} from '@venizia/ignis-helpers';
|
|
2532
|
-
|
|
2533
|
-
// --- Manual checkpoints ---
|
|
2534
|
-
const start = getPerformanceCheckpoint(); // performance.now()
|
|
2535
|
-
|
|
2536
|
-
// ... do work ...
|
|
2537
|
-
|
|
2538
|
-
const elapsed = getExecutedPerformance({ from: start, digit: 3 });
|
|
2539
|
-
// e.g., 142.567 (ms, 3 decimal places)
|
|
2540
|
-
// Default digit: 6
|
|
2541
|
-
|
|
2542
|
-
// --- Automatic measurement with logging ---
|
|
2543
|
-
const result = await executeWithPerformanceMeasure({
|
|
2544
|
-
scope: 'DataSync',
|
|
2545
|
-
description: 'Syncing user data',
|
|
2546
|
-
logger, // Logger instance (falls back to console)
|
|
2547
|
-
level: 'debug', // Log level (default: 'debug')
|
|
2548
|
-
args: { userId: 123 }, // Optional context args (logged)
|
|
2549
|
-
task: () => syncUserData(),
|
|
2550
|
-
});
|
|
2551
|
-
// Logs:
|
|
2552
|
-
// [DataSync] START | Syncing user data... | Args: {"userId":123}
|
|
2553
|
-
// [DataSync] DONE | Syncing user data | Args: {"userId":123} | Took: 142.567 (ms)
|
|
2554
|
-
// Returns: result of syncUserData()
|
|
2555
|
-
```
|
|
2556
|
-
|
|
2557
|
-
### Module Utility
|
|
2558
|
-
|
|
2559
|
-
Pre-flight check for optional dependencies before using them. Throws a clear error with install instructions if any module is missing.
|
|
2560
|
-
|
|
2561
|
-
```typescript
|
|
2562
|
-
import { validateModule } from '@venizia/ignis-helpers';
|
|
2563
|
-
|
|
2564
|
-
// Check single module
|
|
2565
|
-
await validateModule({
|
|
2566
|
-
scope: 'SocketIO',
|
|
2567
|
-
modules: ['socket.io'],
|
|
2568
|
-
});
|
|
2569
|
-
|
|
2570
|
-
// Check multiple modules
|
|
2571
|
-
await validateModule({
|
|
2572
|
-
scope: 'SocketIO',
|
|
2573
|
-
modules: ['socket.io', '@socket.io/redis-adapter'],
|
|
2574
|
-
});
|
|
2575
|
-
// If socket.io is missing:
|
|
2576
|
-
// throws ApplicationError:
|
|
2577
|
-
// "[validateModule] socket.io is required for SocketIO. Please install 'socket.io'"
|
|
2578
|
-
|
|
2579
|
-
// Without scope
|
|
2580
|
-
await validateModule({
|
|
2581
|
-
modules: ['bullmq'],
|
|
2582
|
-
});
|
|
2583
|
-
// If bullmq is missing:
|
|
2584
|
-
// throws: "[validateModule] bullmq is required. Please install 'bullmq'"
|
|
2585
|
-
```
|
|
2586
|
-
|
|
2587
|
-
This utility is used internally by all sub-path modules (BullMQ, MQTT, MinIO, Socket.IO, Axios, Cron, Kafka) to provide clear error messages when peer dependencies are not installed.
|
|
2588
|
-
|
|
2589
|
-
---
|
|
2590
|
-
|
|
2591
|
-
## Common Types
|
|
2592
|
-
|
|
2593
|
-
```typescript
|
|
2594
|
-
// Nullable
|
|
2595
|
-
type TNullable<T> = T | undefined | null;
|
|
2596
|
-
|
|
2597
|
-
// Flexible types
|
|
2598
|
-
type AnyType = any;
|
|
2599
|
-
type AnyObject = Record<string | symbol | number, any>;
|
|
2600
|
-
|
|
2601
|
-
// Value types
|
|
2602
|
-
type ValueOrPromise<T> = T | Promise<T>;
|
|
2603
|
-
type ValueOf<T> = T[keyof T];
|
|
2604
|
-
type ValueOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
2605
|
-
type ValueOptionalExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
|
|
2606
|
-
|
|
2607
|
-
// Class types
|
|
2608
|
-
type TConstructor<T> = new (...args: any[]) => T;
|
|
2609
|
-
type TAbstractConstructor<T> = abstract new (...args: any[]) => T;
|
|
2610
|
-
type TClass<T> = TConstructor<T> & { [property: string]: any };
|
|
2611
|
-
type TAbstractClass<T> = TAbstractConstructor<T> & { [property: string]: any };
|
|
2612
|
-
|
|
2613
|
-
// Mixin types
|
|
2614
|
-
type TMixinTarget<T> = TConstructor<{ [P in keyof T]: T[P] }>;
|
|
2615
|
-
type TAbstractMixinTarget<T> = TAbstractConstructor<{ [P in keyof T]: T[P] }>;
|
|
2616
|
-
|
|
2617
|
-
// Resolver types
|
|
2618
|
-
type TResolver<T> = (...args: any[]) => T;
|
|
2619
|
-
type TAsyncResolver<T> = (...args: any[]) => T | Promise<T>;
|
|
2620
|
-
type TValueOrResolver<T> = T | TResolver<T>;
|
|
2621
|
-
|
|
2622
|
-
// Resolver helpers
|
|
2623
|
-
resolveValue<T>(valueOrResolver) // Resolve sync value or resolver (handles class constructors)
|
|
2624
|
-
resolveValueAsync<T>(valueOrResolver) // Resolve async value or resolver
|
|
2625
|
-
resolveClass<T>(ref) // Resolve class reference or string binding key
|
|
2626
|
-
|
|
2627
|
-
// Configuration
|
|
2628
|
-
interface IConfigurable<Options, Result> {
|
|
2629
|
-
configure(opts?: Options): ValueOrPromise<Result>;
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// Constants
|
|
2633
|
-
RuntimeModules.detect() / .isBun() / .isNode()
|
|
2634
|
-
DataTypes.NUMBER | TEXT | BYTE | JSON | BOOLEAN
|
|
2635
|
-
HTTP.Headers, HTTP.Methods, HTTP.ResultCodes, HTTP.HeaderValues
|
|
2636
|
-
MimeTypes.IMAGE | VIDEO | TEXT | UNKNOWN
|
|
2637
|
-
|
|
2638
|
-
// Field Mapping Types (for dynamic schemas)
|
|
2639
|
-
type TFieldMappingDataType = 'string' | 'number' | 'strings' | 'numbers' | 'boolean';
|
|
2640
|
-
interface IFieldMapping { name: string; type: TFieldMappingDataType; default?: any; }
|
|
2641
|
-
|
|
2642
|
-
// JSX (re-exported from Hono)
|
|
2643
|
-
type { Child, FC, PropsWithChildren } from 'hono/jsx';
|
|
2644
|
-
```
|
|
2645
|
-
|
|
2646
|
-
---
|
|
2647
|
-
|
|
2648
|
-
## Key Patterns
|
|
2649
|
-
|
|
2650
|
-
### BaseHelper
|
|
2651
|
-
|
|
2652
|
-
All helpers extend `BaseHelper` which provides scoped logging automatically:
|
|
2653
|
-
|
|
2654
|
-
```typescript
|
|
2655
|
-
class BaseHelper {
|
|
2656
|
-
scope: string;
|
|
2657
|
-
identifier: string;
|
|
2658
|
-
logger: Logger;
|
|
2659
|
-
|
|
2660
|
-
constructor(opts: { scope: string; identifier?: string });
|
|
2661
|
-
|
|
2662
|
-
getIdentifier(): string;
|
|
2663
|
-
getLogger(): Logger;
|
|
2664
|
-
}
|
|
2665
|
-
```
|
|
2666
|
-
|
|
2667
|
-
Usage in helpers:
|
|
2668
|
-
|
|
2669
|
-
```typescript
|
|
2670
|
-
class MyHelper extends BaseHelper {
|
|
2671
|
-
constructor() {
|
|
2672
|
-
super({ scope: 'MyHelper', identifier: 'instance-1' });
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
doWork() {
|
|
2676
|
-
this.logger.for('doWork').info('Starting...');
|
|
2677
|
-
this.logger.for('doWork').debug('Detail: %j', data);
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
```
|
|
2681
|
-
|
|
2682
|
-
The logger scope is constructed as `[scope-identifier]` (or just `[scope]` if no identifier). Method-scoped loggers append `-methodName`.
|
|
2683
|
-
|
|
2684
|
-
### Options Objects
|
|
2685
|
-
|
|
2686
|
-
All APIs use options objects instead of positional parameters:
|
|
2687
|
-
|
|
2688
|
-
```typescript
|
|
2689
|
-
// Correct
|
|
2690
|
-
redis.get({ key: 'user:1' });
|
|
2691
|
-
aes.encrypt({ message: 'data', secret: 'key' });
|
|
2692
|
-
pool.register({ key: 'worker-1', worker: myWorker });
|
|
2693
|
-
|
|
2694
|
-
// NOT positional arguments
|
|
2695
|
-
// redis.get('user:1'); -- wrong
|
|
2696
|
-
// aes.encrypt('data', 'key') -- wrong
|
|
2697
|
-
```
|
|
2698
|
-
|
|
2699
|
-
### Event Lifecycle
|
|
2700
|
-
|
|
2701
|
-
Helpers that manage connections follow a consistent lifecycle hook pattern:
|
|
2702
|
-
|
|
2703
|
-
```typescript
|
|
2704
|
-
{
|
|
2705
|
-
onInitialized?: (opts) => void; // Instance created
|
|
2706
|
-
onConnected?: (opts) => void; // Connection established
|
|
2707
|
-
onReady?: (opts) => void; // Ready for operations
|
|
2708
|
-
onError?: (opts) => void; // Error occurred
|
|
2709
|
-
}
|
|
2710
|
-
```
|
|
2711
|
-
|
|
2712
|
-
### IConfigurable
|
|
2713
|
-
|
|
2714
|
-
Helpers that require async initialization implement `IConfigurable`:
|
|
2715
|
-
|
|
2716
|
-
```typescript
|
|
2717
|
-
interface IConfigurable<Options, Result> {
|
|
2718
|
-
configure(opts?: Options): ValueOrPromise<Result>;
|
|
2719
|
-
}
|
|
2720
|
-
```
|
|
2721
|
-
|
|
2722
|
-
---
|
|
2723
|
-
|
|
2724
|
-
## Integration with IGNIS Core
|
|
2725
|
-
|
|
2726
|
-
The helpers package is designed to integrate with the IGNIS framework's IoC container. Here is how helpers are typically used within an IGNIS application:
|
|
2727
|
-
|
|
2728
|
-
**Binding helpers in the application lifecycle:**
|
|
2729
|
-
|
|
2730
|
-
```typescript
|
|
2731
|
-
import { BaseApplication } from '@venizia/ignis';
|
|
2732
|
-
import { BootMixin } from '@venizia/ignis-boot';
|
|
2733
|
-
import { RedisSingleHelper } from '@venizia/ignis-helpers';
|
|
2734
|
-
|
|
2735
|
-
class MyApplication extends BootMixin(BaseApplication) {
|
|
2736
|
-
async preConfigure() {
|
|
2737
|
-
// BaseApplication extends the IoC Container directly -- bind on `this`, not `this.container`.
|
|
2738
|
-
this.bind({ key: 'datasources.Redis' }).toValue(
|
|
2739
|
-
new RedisSingleHelper({
|
|
2740
|
-
name: 'main',
|
|
2741
|
-
host: process.env.REDIS_HOST,
|
|
2742
|
-
port: parseInt(process.env.REDIS_PORT),
|
|
2743
|
-
password: process.env.REDIS_PASSWORD,
|
|
2744
|
-
})
|
|
2745
|
-
);
|
|
2746
|
-
|
|
2747
|
-
// Register other helpers...
|
|
2748
|
-
await super.preConfigure();
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
```
|
|
2752
|
-
|
|
2753
|
-
**Injecting helpers in services:**
|
|
2754
|
-
|
|
2755
|
-
```typescript
|
|
2756
|
-
import { inject } from '@venizia/ignis-inversion';
|
|
2757
|
-
import { BaseService } from '@venizia/ignis';
|
|
2758
|
-
import { IRedisHelper } from '@venizia/ignis-helpers';
|
|
2759
|
-
|
|
2760
|
-
export class CacheService extends BaseService {
|
|
2761
|
-
constructor(
|
|
2762
|
-
@inject({ key: 'datasources.Redis' }) private redis: IRedisHelper,
|
|
2763
|
-
) {
|
|
2764
|
-
super({ scope: CacheService.name });
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
async getCached(key: string) {
|
|
2768
|
-
return this.redis.getObject({ key });
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
```
|
|
2772
|
-
|
|
2773
|
-
**Using helpers in controllers:**
|
|
2774
|
-
|
|
2775
|
-
```typescript
|
|
2776
|
-
import { BaseRestController, controller, get, jsonResponse, TRouteContext } from '@venizia/ignis';
|
|
2777
|
-
import { inject } from '@venizia/ignis-inversion';
|
|
2778
|
-
import { IRedisHelper } from '@venizia/ignis-helpers';
|
|
2779
|
-
import { z } from '@hono/zod-openapi';
|
|
2780
|
-
|
|
2781
|
-
@controller({ path: '/health' })
|
|
2782
|
-
export class HealthController extends BaseRestController {
|
|
2783
|
-
constructor(
|
|
2784
|
-
@inject({ key: 'datasources.Redis' }) private redis: IRedisHelper,
|
|
2785
|
-
) {
|
|
2786
|
-
super({ scope: HealthController.name });
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
override binding() {}
|
|
2790
|
-
|
|
2791
|
-
@get({
|
|
2792
|
-
configs: {
|
|
2793
|
-
path: '/',
|
|
2794
|
-
responses: jsonResponse({ schema: z.object({ redis: z.string() }) }),
|
|
2795
|
-
},
|
|
2796
|
-
})
|
|
2797
|
-
async check(context: TRouteContext) {
|
|
2798
|
-
const ping = await this.redis.ping();
|
|
2799
|
-
return context.json({ redis: ping === 'PONG' ? 'up' : 'down' }, 200);
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
```
|
|
2803
|
-
|
|
2804
|
-
---
|
|
2805
|
-
|
|
2806
|
-
## Environment Variables Reference
|
|
2807
|
-
|
|
2808
|
-
Complete table of all environment variables used across all helpers modules.
|
|
2809
|
-
|
|
2810
|
-
### Logger
|
|
2811
|
-
|
|
2812
|
-
| Variable | Default | Module | Description |
|
|
2813
|
-
| ----------------------------------- | -------------- | ------- | ---------------------------------------- |
|
|
2814
|
-
| `APP_ENV_LOGGER_FOLDER_PATH` | `./` | Logger | Directory for log files |
|
|
2815
|
-
| `APP_ENV_LOGGER_FORMAT` | `text` | Logger | Log format: `json` or `text` |
|
|
2816
|
-
| `APP_ENV_LOGGER_FILE_FREQUENCY` | `1h` | Logger | File rotation frequency |
|
|
2817
|
-
| `APP_ENV_LOGGER_FILE_MAX_SIZE` | `100m` | Logger | Max file size before rotation |
|
|
2818
|
-
| `APP_ENV_LOGGER_FILE_MAX_FILES` | `5d` | Logger | Max retention period |
|
|
2819
|
-
| `APP_ENV_LOGGER_FILE_DATE_PATTERN` | `YYYYMMDD_HH` | Logger | Date pattern for filenames |
|
|
2820
|
-
| `APP_ENV_LOGGER_DGRAM_HOST` | -- | Logger | UDP transport host |
|
|
2821
|
-
| `APP_ENV_LOGGER_DGRAM_PORT` | -- | Logger | UDP transport port |
|
|
2822
|
-
| `APP_ENV_LOGGER_DGRAM_LABEL` | -- | Logger | UDP transport label |
|
|
2823
|
-
| `APP_ENV_LOGGER_DGRAM_LEVELS` | -- | Logger | Comma-separated UDP levels |
|
|
2824
|
-
| `DEBUG` | -- | Logger | Enable debug level output |
|
|
2825
|
-
| `APP_ENV_EXTRA_LOG_ENVS` | -- | Logger | Extra NODE_ENV values for debug |
|
|
2826
|
-
|
|
2827
|
-
### Application
|
|
2828
|
-
|
|
2829
|
-
| Variable | Default | Module | Description |
|
|
2830
|
-
| ---------------------------- | ------------------ | --------------- | ------------------------------------- |
|
|
2831
|
-
| `APP_ENV_APPLICATION_NAME` | `APP` | Logger/Defaults | Application name prefix |
|
|
2832
|
-
| `APP_ENV_APPLICATION_TIMEZONE` | `Asia/Ho_Chi_Minh` | Date utility | Default timezone for dayjs |
|
|
2833
|
-
| `APPLICATION_ENV_PREFIX` | `APP_ENV` | Environment | Prefix for ApplicationEnvironment |
|
|
2834
|
-
| `NODE_ENV` | `development` | Environment | Runtime environment name |
|
|
2835
|
-
|
|
2836
|
-
---
|
|
2837
|
-
|
|
2838
|
-
## Error Handling Patterns
|
|
2839
|
-
|
|
2840
|
-
### Connection-based Helpers (Redis, MQTT, TCP, etc.)
|
|
2841
|
-
|
|
2842
|
-
All connection helpers use lifecycle hooks for error handling. Errors are never swallowed -- they are logged and forwarded to your callback:
|
|
2843
|
-
|
|
2844
|
-
```typescript
|
|
2845
|
-
const redis = new RedisSingleHelper({
|
|
2846
|
-
// ...
|
|
2847
|
-
onError: ({ name, helper, error }) => {
|
|
2848
|
-
// Log to external monitoring
|
|
2849
|
-
monitoring.reportError('redis', error);
|
|
2850
|
-
|
|
2851
|
-
// Check if connection is recoverable
|
|
2852
|
-
if (helper.client.status === 'end') {
|
|
2853
|
-
// Connection is permanently closed
|
|
2854
|
-
process.exit(1);
|
|
2855
|
-
}
|
|
2856
|
-
// Otherwise, ioredis will auto-reconnect per retryStrategy
|
|
2857
|
-
},
|
|
2858
|
-
});
|
|
2859
|
-
```
|
|
2860
|
-
|
|
2861
|
-
### Queue Helpers
|
|
2862
|
-
|
|
2863
|
-
BullMQ worker failures are handled per-job:
|
|
2864
|
-
|
|
2865
|
-
```typescript
|
|
2866
|
-
const worker = BullMQHelper.newInstance({
|
|
2867
|
-
// ...
|
|
2868
|
-
onWorkerDataFail: async (job, error) => {
|
|
2869
|
-
// job may be undefined if the failure happened before job was fetched
|
|
2870
|
-
if (job) {
|
|
2871
|
-
console.error(`Job ${job.id} failed (attempt ${job.attemptsMade}):`, error);
|
|
2872
|
-
}
|
|
2873
|
-
// BullMQ automatically retries based on job options (attempts, backoff)
|
|
2874
|
-
},
|
|
2875
|
-
});
|
|
2876
|
-
```
|
|
2877
|
-
|
|
2878
|
-
### Kafka Helpers
|
|
2879
|
-
|
|
2880
|
-
Kafka helpers use try/catch with re-throw. All operations log the error before throwing:
|
|
2881
|
-
|
|
2882
|
-
```typescript
|
|
2883
|
-
try {
|
|
2884
|
-
await producer.send({ messages: [...] });
|
|
2885
|
-
} catch (error) {
|
|
2886
|
-
// The helper already logged:
|
|
2887
|
-
// "[send] Failed to send messages: <error> | ID: <identifier>"
|
|
2888
|
-
// The onError callback was also invoked
|
|
2889
|
-
// The error is re-thrown for your handling
|
|
2890
|
-
}
|
|
2891
|
-
```
|
|
2892
|
-
|
|
2893
|
-
### Storage Helpers
|
|
2894
|
-
|
|
2895
|
-
Storage operations throw `ApplicationError` for validation failures and missing resources:
|
|
2896
|
-
|
|
2897
|
-
```typescript
|
|
2898
|
-
try {
|
|
2899
|
-
await storage.upload({ bucket: 'nonexistent', files: [...] });
|
|
2900
|
-
} catch (error) {
|
|
2901
|
-
// ApplicationError: "[upload] Bucket does not exist | name: nonexistent"
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
try {
|
|
2905
|
-
await storage.createBucket({ name: '../etc/passwd' });
|
|
2906
|
-
} catch (error) {
|
|
2907
|
-
// ApplicationError: "[createBucket] Invalid name to create bucket!"
|
|
2908
|
-
}
|
|
2909
|
-
```
|
|
2910
|
-
|
|
2911
|
-
---
|
|
2912
|
-
|
|
2913
|
-
## Performance Tips
|
|
2914
|
-
|
|
2915
|
-
### Redis Connection Pooling
|
|
2916
|
-
|
|
2917
|
-
- Each Redis helper instance creates a single IoRedis connection by default. For high-throughput, consider multiple instances with different roles (read vs write).
|
|
2918
|
-
- BullMQ calls `.duplicate()` automatically -- it does not share your main connection.
|
|
2919
|
-
- The `maxRetriesPerRequest: null` setting prevents blocking on failed requests.
|
|
2920
|
-
- Use `autoConnect: false` (lazy connect) when you need to control connection timing.
|
|
2921
|
-
|
|
2922
|
-
### Logger Performance
|
|
2923
|
-
|
|
2924
|
-
- `Logger.get()` caching avoids creating new Winston loggers per call.
|
|
2925
|
-
- The `debug()` method has zero overhead when `DEBUG` is not set (check is pre-computed at module load).
|
|
2926
|
-
- For hot paths (>10k calls/sec), use `HfLogger` instead of Winston (~100--300ns vs ~1--10us).
|
|
2927
|
-
- Consider disabling file transports in development for faster startup.
|
|
2928
|
-
|
|
2929
|
-
### Queue Optimization
|
|
2930
|
-
|
|
2931
|
-
- In-memory `QueueHelper` processes one item at a time (sequential). For parallel processing, use BullMQ with `numberOfWorker > 1`.
|
|
2932
|
-
- BullMQ default job options (`removeOnComplete: true`, `removeOnFail: true`) prevent Redis memory growth.
|
|
2933
|
-
- For high-throughput Kafka, tune `highWaterMark` and `maxWaitTime` based on your message sizes.
|
|
2934
|
-
|
|
2935
|
-
### Storage
|
|
2936
|
-
|
|
2937
|
-
- MinIO `listObjects` returns a stream internally -- the helper collects all results. Use `maxKeys` to limit memory usage.
|
|
2938
|
-
- Disk storage validates names before I/O operations, preventing unnecessary filesystem calls.
|
|
2939
|
-
- Use `normalizeNameFn` to control file naming and avoid repeated sanitization.
|
|
2940
|
-
|
|
2941
|
-
### Crypto
|
|
2942
|
-
|
|
2943
|
-
- AES-256-GCM is recommended over CBC for new code (built-in authentication).
|
|
2944
|
-
- RSA key generation is expensive (~50--200ms for 2048-bit). Generate keys once and store them.
|
|
2945
|
-
- ECDH uses Web Crypto API (`crypto.subtle`) which is hardware-accelerated on most platforms.
|
|
2946
|
-
- Pre-encode HfLogger messages and ECDH keys at initialization time, not in hot paths.
|
|
2947
|
-
|
|
2948
|
-
### Network
|
|
2949
|
-
|
|
2950
|
-
- `NodeFetchNetworkRequest` has zero dependencies but no interceptor support.
|
|
2951
|
-
- `AxiosNetworkRequest` supports interceptors but adds the `axios` dependency.
|
|
2952
|
-
- TCP clients with `reconnect: true` use a 5-second fixed delay between reconnection attempts.
|
|
2953
|
-
|
|
2954
|
-
---
|
|
2955
|
-
|
|
2956
|
-
## About IGNIS
|
|
2957
|
-
|
|
2958
|
-
IGNIS brings together the structured, enterprise development experience of **LoopBack 4** with the blazing speed and simplicity of **Hono** -- giving you the best of both worlds.
|
|
2959
|
-
|
|
2960
|
-
## Documentation
|
|
2961
|
-
|
|
2962
|
-
- [IGNIS Repository](https://github.com/VENIZIA-AI/ignis)
|
|
2963
|
-
- [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/get-started/5-minute-quickstart.md)
|
|
2964
|
-
- [Reference Index](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/index.md)
|
|
2965
|
-
|
|
2966
|
-
## License
|
|
2967
|
-
|
|
2968
|
-
[MIT](./LICENSE.md)
|
|
91
|
+
```bash
|
|
92
|
+
# example: pino logging + BullMQ jobs + MinIO storage
|
|
93
|
+
bun add pino bullmq minio
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Everything else - Redis (`ioredis` is a real dependency), disk and in-memory storage, crypto,
|
|
97
|
+
network, UID, env, pool, worker threads - comes straight off the root barrel.
|
|
98
|
+
|
|
99
|
+
## Modules
|
|
100
|
+
|
|
101
|
+
| Module | What it gives you |
|
|
102
|
+
| :--- | :--- |
|
|
103
|
+
| [logger](https://ignis.venizia.ai/extensions/helpers/logger/) | Scoped `ILogger` over a pluggable winston or pino backend, plus `HfLogger` for hot paths |
|
|
104
|
+
| [redis](https://ignis.venizia.ai/extensions/helpers/redis/) | Single, cluster, and sentinel clients over `ioredis`: key-value, hash, RedisJSON, pub/sub |
|
|
105
|
+
| [queue](https://ignis.venizia.ai/extensions/helpers/queue/) | BullMQ jobs, MQTT topics, [Kafka](https://ignis.venizia.ai/extensions/helpers/kafka/) producers and consumers, plus an in-process `SequentialQueueHelper` |
|
|
106
|
+
| [storage](https://ignis.venizia.ai/extensions/helpers/storage/) | One `IStorageHelper` interface over MinIO/S3, Bun S3, local disk, and memory |
|
|
107
|
+
| [crypto](https://ignis.venizia.ai/extensions/helpers/crypto/) | `AES`, `RSA`, `ECDH` - encryption, signing, and key exchange |
|
|
108
|
+
| [cron](https://ignis.venizia.ai/extensions/helpers/cron/) | `CronHelper`: declarative schedules with lifecycle hooks |
|
|
109
|
+
| [socket](https://ignis.venizia.ai/extensions/helpers/socket-io/) | Socket.IO server and client, plus a [Bun-native WebSocket](https://ignis.venizia.ai/extensions/helpers/websocket/) server |
|
|
110
|
+
| [network](https://ignis.venizia.ai/extensions/helpers/network/) | HTTP fetchers (native or axios) and TCP, TLS, UDP clients and servers |
|
|
111
|
+
| [uid](https://ignis.venizia.ai/extensions/helpers/uid/) | `SnowflakeUidHelper`: 70-bit time-sortable IDs with Base62 encoding, ~4M/sec/worker |
|
|
112
|
+
| [env](https://ignis.venizia.ai/extensions/helpers/env/) | `ApplicationEnvironment`: prefixed, typed access to `process.env` |
|
|
113
|
+
| pool | `BasePoolHelper`: generic resource pooling with an O(1) FIFO wait queue |
|
|
114
|
+
| [secrets](https://ignis.venizia.ai/extensions/helpers/secrets/) | `createSecretsHelper`: system envs, HashiCorp Vault, or dotenvx behind one interface |
|
|
115
|
+
| [worker-thread](https://ignis.venizia.ai/extensions/helpers/worker-thread/) | `WorkerPoolHelper` and a typed message bus for CPU-bound work |
|
|
116
|
+
|
|
117
|
+
Also on the root barrel: [error](https://ignis.venizia.ai/extensions/helpers/error/) helpers
|
|
118
|
+
(`getError`, `ApplicationError`, `isApplicationError`) and utilities for dates, parsing, promises,
|
|
119
|
+
retries, and requests.
|
|
120
|
+
|
|
121
|
+
## Logging
|
|
122
|
+
|
|
123
|
+
Consumers type against **`ILogger`** and never name a concrete logger class. Get one three ways:
|
|
124
|
+
|
|
125
|
+
| Source | When |
|
|
126
|
+
| :--- | :--- |
|
|
127
|
+
| `this.logger` | Inside anything extending `BaseHelper` - already scoped |
|
|
128
|
+
| `LoggerFactory.getLogger(['Service', 'method'])` | Anywhere; scopes join with `-` |
|
|
129
|
+
| `ApplicationLogger.get('MyService')` | Single-scope shorthand |
|
|
130
|
+
|
|
131
|
+
`logger.for('methodName')` returns a child logger scoped to that method.
|
|
132
|
+
|
|
133
|
+
**Exactly five levels**, in severity order: `debug`, `info`, `warn`, `error`, `emerg`. `debug()` is
|
|
134
|
+
gated once at module load by `DEBUG` plus an `NODE_ENV` allowlist.
|
|
135
|
+
|
|
136
|
+
**Provider selection** happens once, at the entrypoint, through `LoggerFactory.use({ provider })`.
|
|
137
|
+
The factory hands out stable wrappers that resolve their backend lazily, so loggers acquired before
|
|
138
|
+
`use()` re-point to the new provider. With no provider registered, winston loads on the first log
|
|
139
|
+
call.
|
|
140
|
+
|
|
141
|
+
> [!IMPORTANT]
|
|
142
|
+
> Compiled binaries (`bun build --compile`) **must** call `LoggerFactory.use()` explicitly. The
|
|
143
|
+
> winston fallback is a runtime `require`, which a bundler cannot see - only a class reference
|
|
144
|
+
> carries a provider into a bundle.
|
|
145
|
+
|
|
146
|
+
`HfLogger` is a separate ring-buffered logger for hot paths where allocation matters, drained by
|
|
147
|
+
`HfLogFlusher`. Reach for it when profiling says to, not before.
|
|
148
|
+
|
|
149
|
+
### Environment variables
|
|
150
|
+
|
|
151
|
+
| Variable | Default | Effect |
|
|
152
|
+
| :--- | :--- | :--- |
|
|
153
|
+
| `APP_ENV_LOGGER_LEVEL` | `debug` | Floor below which a line reaches no transport |
|
|
154
|
+
| `APP_ENV_LOGGER_FORMAT` | `text` (winston) / `json` (pino) | `json` or `text`; pino needs `pino-pretty` for `text` |
|
|
155
|
+
| `APP_ENV_LOGGER_FOLDER_PATH` | unset | Set it to enable rotating file output; unset means console or stdout only |
|
|
156
|
+
| `APP_ENV_LOGGER_FILE_FREQUENCY` | `1h` | Rotation interval |
|
|
157
|
+
| `APP_ENV_LOGGER_FILE_MAX_SIZE` | `100m` | Size at which a file rotates |
|
|
158
|
+
| `APP_ENV_LOGGER_FILE_MAX_FILES` | `5d` | Retention |
|
|
159
|
+
| `APP_ENV_LOGGER_FILE_DATE_PATTERN` | `YYYYMMDD_HH` | Filename date pattern (winston only) |
|
|
160
|
+
| `APP_ENV_LOGGER_DGRAM_HOST` / `_PORT` / `_LABEL` / `_LEVELS` | unset | UDP transport for remote log shipping (winston only) |
|
|
161
|
+
| `APP_ENV_LOGGER_DO_REDACT` | `true` | Set `false` to stop redacting secret-looking keys |
|
|
162
|
+
| `APP_ENV_LOGGER_INSPECT_DEPTH` | `5` | Depth before a nested object collapses to `[Object]` |
|
|
163
|
+
| `DEBUG` + `NODE_ENV` | unset | Together gate `debug()`; read once at module load |
|
|
164
|
+
|
|
165
|
+
Full detail: [Logger reference](https://ignis.venizia.ai/extensions/helpers/logger/).
|
|
166
|
+
|
|
167
|
+
## Requirements
|
|
168
|
+
|
|
169
|
+
- **Bun >= 1.3.** Node.js 18+ runs most modules; the Bun-native ones (`/bun-s3`, the WebSocket
|
|
170
|
+
server) need Bun.
|
|
171
|
+
- `experimentalDecorators` and `emitDecoratorMetadata` must be `true` in your `tsconfig.json`,
|
|
172
|
+
declared **inline** - Bun does not resolve them through `extends`. Copy them from
|
|
173
|
+
`@venizia/dev-configs/tsconfig.common.json`.
|
|
174
|
+
- Errors always go through `getError` / `ApplicationError`, never `new Error`. Across package
|
|
175
|
+
boundaries check with `isApplicationError()`, never `instanceof` - the class has more than one
|
|
176
|
+
identity in a monorepo.
|
|
177
|
+
|
|
178
|
+
## Links
|
|
179
|
+
|
|
180
|
+
[Helpers documentation](https://ignis.venizia.ai/extensions/helpers/) •
|
|
181
|
+
[All extensions](https://ignis.venizia.ai/extensions/) •
|
|
182
|
+
[Best practices](https://ignis.venizia.ai/best-practices/) •
|
|
183
|
+
[Changelog](https://ignis.venizia.ai/changelogs/) •
|
|
184
|
+
[Issues](https://github.com/VENIZIA-AI/ignis/issues)
|
|
185
|
+
|
|
186
|
+
MIT licensed - see [LICENSE.md](LICENSE.md). Questions: developer@venizia.ai
|
|
187
|
+
</content>
|