Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 048f038e36 | |||
| e375adb80a | |||
| 9d7da5bc25 | |||
| 41fe7a8a47 |
21
changelog.md
21
changelog.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-10 - 4.1.1 - fix(smartproxy)
|
||||||
|
upgrade @push.rocks/smartproxy to ^23.1.0 and adapt code/tests for its async getStatistics() API
|
||||||
|
|
||||||
|
- Bumped dependency @push.rocks/smartproxy 22.4.2 → 23.1.0 in package.json
|
||||||
|
- Changed ts/monitoring/classes.metricsmanager.ts to await smartProxy.getStatistics() (was synchronous)
|
||||||
|
- Updated multiple tests to set cacheConfig: { enabled: false } and added socketTimeouts where appropriate
|
||||||
|
- Improved SMTP test servers: handle multi-line input, drop data for packet-loss simulation, and ignore socket errors to make tests more robust
|
||||||
|
- Added migration notes to readme.hints.md documenting SmartProxy v23.1.0 changes (async getStatistics, Rust proxy behavior)
|
||||||
|
|
||||||
|
## 2026-02-10 - 4.1.0 - feat(cache)
|
||||||
|
add persistent smartdata-backed cache with LocalTsmDb, cache cleaner, and DcRouter integration
|
||||||
|
|
||||||
|
- Introduce CacheDb and CacheCleaner using @push.rocks/smartdata and @push.rocks/smartmongo (LocalTsmDb) for persistent caching
|
||||||
|
- Integrate cache initialization, console summary, and graceful shutdown into DcRouter (options.cacheConfig and setupCacheDb())
|
||||||
|
- Require svDb() decorators on concrete cache document classes; remove decorators from the abstract CachedDocument base class
|
||||||
|
- Switch CacheCleaner to smartdata getInstances() + per-document delete() instead of deleteMany
|
||||||
|
- Adapt to LocalTsmDb API changes (folderPath option and start() returning connectionUri) and initialize SmartdataDb with mongoDbUrl/mongoDbName
|
||||||
|
- Remove experimentalDecorators and emitDecoratorMetadata from tsconfig to use TC39 Stage 3 decorators (smartdata v7+ compatibility)
|
||||||
|
- Add package.json exports mapping (remove main/typings entries) to expose dist entry points
|
||||||
|
- Add README documentation for the Smartdata Cache System and configuration/usage examples
|
||||||
|
|
||||||
## 2026-02-03 - 4.0.0 - BREAKING CHANGE(config)
|
## 2026-02-03 - 4.0.0 - BREAKING CHANGE(config)
|
||||||
convert configuration management to read-only; remove updateConfiguration endpoint and client-side editing
|
convert configuration management to read-only; remove updateConfiguration endpoint and client-side editing
|
||||||
|
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "4.0.0",
|
"version": "4.1.1",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"main": "dist_ts/index.js",
|
|
||||||
"typings": "dist_ts/index.d.ts",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist_ts/index.js",
|
||||||
|
"./interfaces": "./dist_ts_interfaces/index.js"
|
||||||
|
},
|
||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^22.4.2",
|
"@push.rocks/smartproxy": "^23.1.0",
|
||||||
"@push.rocks/smartradius": "^1.1.0",
|
"@push.rocks/smartradius": "^1.1.0",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrule": "^2.0.1",
|
"@push.rocks/smartrule": "^2.0.1",
|
||||||
|
|||||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^22.4.2
|
specifier: ^23.1.0
|
||||||
version: 22.4.2(socks@2.8.7)
|
version: 23.1.0(socks@2.8.7)
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
@@ -677,6 +677,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
'@isaacs/brace-expansion@5.0.1':
|
||||||
|
resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1075,8 +1079,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@22.4.2':
|
'@push.rocks/smartproxy@23.1.0':
|
||||||
resolution: {integrity: sha512-JdDa1VxGOnWfF5HuJRvkX3/zHuIKz+IV9n/XOsNZQA9zMZdLVlWPqjGio9GLWsPOWA2l1YZKymjMH4ybPbGQtA==}
|
resolution: {integrity: sha512-2EhMFeQytDwnqooK9BNkLw9oz8M1LUFuMEg6271xRnwf8gUkDq5WT0brrmLdOmpqkU/3h/wDeZUrn65zq3VAcA==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -1099,6 +1103,9 @@ packages:
|
|||||||
'@push.rocks/smartrule@2.0.1':
|
'@push.rocks/smartrule@2.0.1':
|
||||||
resolution: {integrity: sha512-8oYEnS9z+NgCAcUtXPMguYyZpHqA/ROp0bxVQwUaHDwa3YzzA8jHIXvA94hk3sxvkk0xmIpp4UhBEelzIwwJow==}
|
resolution: {integrity: sha512-8oYEnS9z+NgCAcUtXPMguYyZpHqA/ROp0bxVQwUaHDwa3YzzA8jHIXvA94hk3sxvkk0xmIpp4UhBEelzIwwJow==}
|
||||||
|
|
||||||
|
'@push.rocks/smartrust@1.1.1':
|
||||||
|
resolution: {integrity: sha512-NtfTOrVpw0K+z/jW24OmunvZBqkJHfe1tJhTMPFYUb4a5Yt5mtTc3oUvlX+bHarn94Jq0oh0HCLh8xcPQ2Sd7w==}
|
||||||
|
|
||||||
'@push.rocks/smartrx@3.0.10':
|
'@push.rocks/smartrx@3.0.10':
|
||||||
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
||||||
|
|
||||||
@@ -1160,6 +1167,9 @@ packages:
|
|||||||
'@push.rocks/taskbuffer@3.5.0':
|
'@push.rocks/taskbuffer@3.5.0':
|
||||||
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@4.2.0':
|
||||||
|
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
|
||||||
|
|
||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
||||||
|
|
||||||
@@ -3336,6 +3346,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
minimatch@10.1.2:
|
||||||
|
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
@@ -5551,6 +5565,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/balanced-match': 4.0.1
|
'@isaacs/balanced-match': 4.0.1
|
||||||
|
|
||||||
|
'@isaacs/brace-expansion@5.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/balanced-match': 4.0.1
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
@@ -6513,7 +6531,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@22.4.2(socks@2.8.7)':
|
'@push.rocks/smartproxy@23.1.0(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartacme': 8.0.0(socks@2.8.7)
|
'@push.rocks/smartacme': 8.0.0(socks@2.8.7)
|
||||||
@@ -6524,13 +6542,14 @@ snapshots:
|
|||||||
'@push.rocks/smartnetwork': 4.4.0
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
|
'@push.rocks/smartrust': 1.1.1
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
'@push.rocks/taskbuffer': 4.2.0
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
'@types/minimatch': 6.0.0
|
'@types/minimatch': 6.0.0
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
minimatch: 10.1.1
|
minimatch: 10.1.2
|
||||||
pretty-ms: 9.3.0
|
pretty-ms: 9.3.0
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6604,6 +6623,10 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartrule@2.0.1': {}
|
'@push.rocks/smartrule@2.0.1': {}
|
||||||
|
|
||||||
|
'@push.rocks/smartrust@1.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
'@push.rocks/smartrx@3.0.10':
|
'@push.rocks/smartrx@3.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -6775,6 +6798,22 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@4.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@design.estate/dees-element': 2.1.6
|
||||||
|
'@push.rocks/lik': 6.2.2
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartlog': 3.1.10
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
'@push.rocks/smarttime': 4.1.1
|
||||||
|
'@push.rocks/smartunique': 3.0.9
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -7631,7 +7670,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
'@types/minimatch@6.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 10.1.1
|
minimatch: 10.1.2
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
@@ -9397,6 +9436,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/brace-expansion': 5.0.0
|
'@isaacs/brace-expansion': 5.0.0
|
||||||
|
|
||||||
|
minimatch@10.1.2:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/brace-expansion': 5.0.1
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
|
|||||||
106
readme.hints.md
106
readme.hints.md
@@ -1,5 +1,28 @@
|
|||||||
# Implementation Hints and Learnings
|
# Implementation Hints and Learnings
|
||||||
|
|
||||||
|
## Dependency Upgrade (2026-02-10)
|
||||||
|
|
||||||
|
### SmartProxy v23.1.0 Upgrade
|
||||||
|
- `@push.rocks/smartproxy`: 22.4.2 → 23.1.0
|
||||||
|
|
||||||
|
**Key Changes:**
|
||||||
|
- Rust-based proxy components for improved performance
|
||||||
|
- Rust binary runs as separate process via IPC
|
||||||
|
- `getStatistics()` now returns `Promise<any>` (was synchronous)
|
||||||
|
- nftables-proxy removed (not used by dcrouter)
|
||||||
|
|
||||||
|
**Code Changes Required:**
|
||||||
|
```typescript
|
||||||
|
// Old (synchronous)
|
||||||
|
const proxyStats = this.dcRouter.smartProxy.getStatistics();
|
||||||
|
|
||||||
|
// New (async)
|
||||||
|
const proxyStats = await this.dcRouter.smartProxy.getStatistics();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files Modified:**
|
||||||
|
- `ts/monitoring/classes.metricsmanager.ts` - Added `await` to `getStatistics()` call
|
||||||
|
|
||||||
## Dependency Upgrade (2026-02-01)
|
## Dependency Upgrade (2026-02-01)
|
||||||
|
|
||||||
### Major Upgrades Completed
|
### Major Upgrades Completed
|
||||||
@@ -1317,3 +1340,86 @@ The configuration UI has been converted from an editable interface to a read-onl
|
|||||||
- Byte sizes auto-formatted (B, KB, MB, GB)
|
- Byte sizes auto-formatted (B, KB, MB, GB)
|
||||||
- Time values shown with "seconds" suffix
|
- Time values shown with "seconds" suffix
|
||||||
- Nested objects with visual indentation
|
- Nested objects with visual indentation
|
||||||
|
|
||||||
|
## Smartdata Cache System (2026-02-03)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
DcRouter now uses smartdata + LocalTsmDb for persistent caching. Data is stored at `/etc/dcrouter/tsmdb`.
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
| Layer | Package | Purpose |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| ORM | `@push.rocks/smartdata` | Document classes, decorators, queries |
|
||||||
|
| Database | `@push.rocks/smartmongo` (LocalTsmDb) | Embedded TsmDB via Unix socket |
|
||||||
|
|
||||||
|
### TC39 Decorators
|
||||||
|
The project uses TC39 Stage 3 decorators (not experimental decorators). The tsconfig was updated:
|
||||||
|
- Removed `experimentalDecorators: true`
|
||||||
|
- Removed `emitDecoratorMetadata: true`
|
||||||
|
|
||||||
|
This is required for smartdata v7+ compatibility.
|
||||||
|
|
||||||
|
### Cache Document Classes
|
||||||
|
Located in `ts/cache/documents/`:
|
||||||
|
|
||||||
|
| Class | Purpose | Default TTL |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `CachedEmail` | Email queue items | 30 days |
|
||||||
|
| `CachedIPReputation` | IP reputation lookups | 24 hours |
|
||||||
|
| `CachedBounce` | Bounce records | 30 days |
|
||||||
|
| `CachedSuppression` | Suppression list | 30 days / permanent |
|
||||||
|
| `CachedDKIMKey` | DKIM key pairs | 90 days |
|
||||||
|
|
||||||
|
### Usage Pattern
|
||||||
|
```typescript
|
||||||
|
// Document classes use smartdata decorators
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class CachedEmail extends CachedDocument<CachedEmail> {
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
|
||||||
|
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id: string;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query examples
|
||||||
|
const email = await CachedEmail.getInstance({ id: 'abc123' });
|
||||||
|
const pending = await CachedEmail.getInstances({ status: 'pending' });
|
||||||
|
await email.save();
|
||||||
|
await email.delete();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
```typescript
|
||||||
|
const dcRouter = new DcRouter({
|
||||||
|
cacheConfig: {
|
||||||
|
enabled: true,
|
||||||
|
storagePath: '/etc/dcrouter/tsmdb',
|
||||||
|
dbName: 'dcrouter',
|
||||||
|
cleanupIntervalHours: 1,
|
||||||
|
ttlConfig: {
|
||||||
|
emails: 30, // days
|
||||||
|
ipReputation: 1, // days
|
||||||
|
bounces: 30, // days
|
||||||
|
dkimKeys: 90, // days
|
||||||
|
suppression: 30 // days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Cleaner
|
||||||
|
- Runs hourly by default (configurable via `cleanupIntervalHours`)
|
||||||
|
- Finds and deletes documents where `expiresAt < now()`
|
||||||
|
- Uses smartdata's `getInstances()` + `delete()` pattern
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
- `ts/cache/classes.cachedb.ts` - CacheDb singleton wrapper
|
||||||
|
- `ts/cache/classes.cached.document.ts` - Base class with TTL support
|
||||||
|
- `ts/cache/classes.cache.cleaner.ts` - Periodic cleanup service
|
||||||
|
- `ts/cache/documents/*.ts` - Document class definitions
|
||||||
@@ -197,25 +197,15 @@ tap.test('CERR-03: Network Failures - should handle EHOSTUNREACH', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle packet loss simulation', async () => {
|
tap.test('CERR-03: Network Failures - should handle packet loss simulation', async () => {
|
||||||
// Create a server that randomly drops data
|
// Create a server that sends a greeting but never responds to commands,
|
||||||
let packetCount = 0;
|
// simulating complete packet loss after the initial connection.
|
||||||
const lossyServer = net.createServer((socket) => {
|
const lossyServer = net.createServer((socket) => {
|
||||||
|
socket.on('error', () => {});
|
||||||
socket.write('220 Lossy server ready\r\n');
|
socket.write('220 Lossy server ready\r\n');
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
// Never respond to any commands - simulates total packet loss
|
||||||
packetCount++;
|
socket.on('data', () => {
|
||||||
|
// Intentionally drop all data to simulate packet loss
|
||||||
// Simulate 30% packet loss
|
|
||||||
if (Math.random() > 0.3) {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, don't respond (simulate packet loss)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,8 +217,8 @@ tap.test('CERR-03: Network Failures - should handle packet loss simulation', asy
|
|||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 2558,
|
port: 2558,
|
||||||
secure: false,
|
secure: false,
|
||||||
connectionTimeout: 1000,
|
connectionTimeout: 2000,
|
||||||
socketTimeout: 1000 // Short timeout to detect loss
|
socketTimeout: 2000 // Short timeout to detect loss
|
||||||
});
|
});
|
||||||
|
|
||||||
let verifyResult = false;
|
let verifyResult = false;
|
||||||
@@ -237,16 +227,16 @@ tap.test('CERR-03: Network Failures - should handle packet loss simulation', asy
|
|||||||
try {
|
try {
|
||||||
verifyResult = await client.verify();
|
verifyResult = await client.verify();
|
||||||
if (verifyResult) {
|
if (verifyResult) {
|
||||||
console.log('✅ Connected despite simulated packet loss');
|
console.log('Connection succeeded unexpectedly');
|
||||||
} else {
|
} else {
|
||||||
console.log('✅ Connection failed due to packet loss');
|
console.log('✅ Connection failed due to packet loss');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorOccurred = true;
|
errorOccurred = true;
|
||||||
console.log(`✅ Packet loss detected after ${packetCount} packets: ${error.message}`);
|
console.log(`✅ Packet loss detected: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either verification failed or an error occurred - both are expected with packet loss
|
// verify() must have returned false or thrown - both indicate packet loss was detected
|
||||||
expect(!verifyResult || errorOccurred).toBeTrue();
|
expect(!verifyResult || errorOccurred).toBeTrue();
|
||||||
|
|
||||||
// Clean up client first
|
// Clean up client first
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ tap.test('setup - start SMTP server for greylisting tests', async () => {
|
|||||||
tap.test('CERR-04: Basic greylisting response handling', async () => {
|
tap.test('CERR-04: Basic greylisting response handling', async () => {
|
||||||
// Create server that simulates greylisting
|
// Create server that simulates greylisting
|
||||||
const greylistServer = net.createServer((socket) => {
|
const greylistServer = net.createServer((socket) => {
|
||||||
|
socket.on('error', () => {});
|
||||||
socket.write('220 Greylist Test Server\r\n');
|
socket.write('220 Greylist Test Server\r\n');
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
const command = data.toString().trim();
|
const lines = data.toString().split('\r\n').filter((l: string) => l.trim());
|
||||||
|
for (const line of lines) {
|
||||||
|
const command = line.trim();
|
||||||
|
|
||||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||||
socket.write('250 OK\r\n');
|
socket.write('250 OK\r\n');
|
||||||
@@ -31,12 +34,17 @@ tap.test('CERR-04: Basic greylisting response handling', async () => {
|
|||||||
} else if (command.startsWith('RCPT TO')) {
|
} else if (command.startsWith('RCPT TO')) {
|
||||||
// Simulate greylisting response
|
// Simulate greylisting response
|
||||||
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
|
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
|
||||||
|
} else if (command.startsWith('RSET')) {
|
||||||
|
socket.write('250 OK\r\n');
|
||||||
|
} else if (command.startsWith('DATA')) {
|
||||||
|
socket.write('503 Bad sequence of commands\r\n');
|
||||||
} else if (command === 'QUIT') {
|
} else if (command === 'QUIT') {
|
||||||
socket.write('221 Bye\r\n');
|
socket.write('221 Bye\r\n');
|
||||||
socket.end();
|
socket.end();
|
||||||
} else {
|
} else if (command.length > 0) {
|
||||||
socket.write('250 OK\r\n');
|
socket.write('250 OK\r\n');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +56,8 @@ tap.test('CERR-04: Basic greylisting response handling', async () => {
|
|||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 2560,
|
port: 2560,
|
||||||
secure: false,
|
secure: false,
|
||||||
connectionTimeout: 5000
|
connectionTimeout: 5000,
|
||||||
|
socketTimeout: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
const email = new Email({
|
const email = new Email({
|
||||||
@@ -107,20 +116,30 @@ tap.test('CERR-04: Different greylisting response codes', async () => {
|
|||||||
tap.test('CERR-04: Greylisting with temporary failure', async () => {
|
tap.test('CERR-04: Greylisting with temporary failure', async () => {
|
||||||
// Create server that sends 450 response (temporary failure)
|
// Create server that sends 450 response (temporary failure)
|
||||||
const tempFailServer = net.createServer((socket) => {
|
const tempFailServer = net.createServer((socket) => {
|
||||||
|
socket.on('error', () => {});
|
||||||
socket.write('220 Temp Fail Server\r\n');
|
socket.write('220 Temp Fail Server\r\n');
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
const command = data.toString().trim();
|
const lines = data.toString().split('\r\n').filter((l: string) => l.trim());
|
||||||
|
for (const line of lines) {
|
||||||
|
const command = line.trim();
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||||
socket.write('250 OK\r\n');
|
socket.write('250 OK\r\n');
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
} else if (command.startsWith('MAIL FROM')) {
|
||||||
socket.write('250 OK\r\n');
|
socket.write('250 OK\r\n');
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
} else if (command.startsWith('RCPT TO')) {
|
||||||
socket.write('450 4.7.1 Mailbox temporarily unavailable\r\n');
|
socket.write('450 4.7.1 Mailbox temporarily unavailable\r\n');
|
||||||
|
} else if (command.startsWith('RSET')) {
|
||||||
|
socket.write('250 OK\r\n');
|
||||||
|
} else if (command.startsWith('DATA')) {
|
||||||
|
socket.write('503 Bad sequence of commands\r\n');
|
||||||
} else if (command === 'QUIT') {
|
} else if (command === 'QUIT') {
|
||||||
socket.write('221 Bye\r\n');
|
socket.write('221 Bye\r\n');
|
||||||
socket.end();
|
socket.end();
|
||||||
|
} else if (command.length > 0) {
|
||||||
|
socket.write('250 OK\r\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -133,7 +152,8 @@ tap.test('CERR-04: Greylisting with temporary failure', async () => {
|
|||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 2561,
|
port: 2561,
|
||||||
secure: false,
|
secure: false,
|
||||||
connectionTimeout: 5000
|
connectionTimeout: 5000,
|
||||||
|
socketTimeout: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
const email = new Email({
|
const email = new Email({
|
||||||
@@ -199,20 +219,30 @@ tap.test('CERR-04: Basic connection verification', async () => {
|
|||||||
tap.test('CERR-04: Server with RCPT rejection', async () => {
|
tap.test('CERR-04: Server with RCPT rejection', async () => {
|
||||||
// Test server rejecting at RCPT TO stage
|
// Test server rejecting at RCPT TO stage
|
||||||
const rejectServer = net.createServer((socket) => {
|
const rejectServer = net.createServer((socket) => {
|
||||||
|
socket.on('error', () => {});
|
||||||
socket.write('220 Reject Server\r\n');
|
socket.write('220 Reject Server\r\n');
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
const command = data.toString().trim();
|
const lines = data.toString().split('\r\n').filter((l: string) => l.trim());
|
||||||
|
for (const line of lines) {
|
||||||
|
const command = line.trim();
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||||
socket.write('250 OK\r\n');
|
socket.write('250 OK\r\n');
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
} else if (command.startsWith('MAIL FROM')) {
|
||||||
socket.write('250 OK\r\n');
|
socket.write('250 OK\r\n');
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
} else if (command.startsWith('RCPT TO')) {
|
||||||
socket.write('451 4.2.1 Recipient rejected temporarily\r\n');
|
socket.write('451 4.2.1 Recipient rejected temporarily\r\n');
|
||||||
|
} else if (command.startsWith('RSET')) {
|
||||||
|
socket.write('250 OK\r\n');
|
||||||
|
} else if (command.startsWith('DATA')) {
|
||||||
|
socket.write('503 Bad sequence of commands\r\n');
|
||||||
} else if (command === 'QUIT') {
|
} else if (command === 'QUIT') {
|
||||||
socket.write('221 Bye\r\n');
|
socket.write('221 Bye\r\n');
|
||||||
socket.end();
|
socket.end();
|
||||||
|
} else if (command.length > 0) {
|
||||||
|
socket.write('250 OK\r\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -225,7 +255,8 @@ tap.test('CERR-04: Server with RCPT rejection', async () => {
|
|||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 2562,
|
port: 2562,
|
||||||
secure: false,
|
secure: false,
|
||||||
connectionTimeout: 5000
|
connectionTimeout: 5000,
|
||||||
|
socketTimeout: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
const email = new Email({
|
const email = new Email({
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ tap.test('should NOT instantiate DNS server when dnsNsDomains is not set', async
|
|||||||
dcRouter = new DcRouter({
|
dcRouter = new DcRouter({
|
||||||
smartProxyConfig: {
|
smartProxyConfig: {
|
||||||
routes: []
|
routes: []
|
||||||
}
|
},
|
||||||
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
await dcRouter.start();
|
await dcRouter.start();
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ tap.test('should use traditional port forwarding when useSocketHandler is false'
|
|||||||
},
|
},
|
||||||
smartProxyConfig: {
|
smartProxyConfig: {
|
||||||
routes: []
|
routes: []
|
||||||
}
|
},
|
||||||
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
await dcRouter.start();
|
await dcRouter.start();
|
||||||
@@ -51,7 +52,8 @@ tap.test('should use socket-handler mode when useSocketHandler is true', async (
|
|||||||
},
|
},
|
||||||
smartProxyConfig: {
|
smartProxyConfig: {
|
||||||
routes: []
|
routes: []
|
||||||
}
|
},
|
||||||
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
await dcRouter.start();
|
await dcRouter.start();
|
||||||
@@ -85,7 +87,7 @@ tap.test('should generate correct email routes for each port', async () => {
|
|||||||
useSocketHandler: true
|
useSocketHandler: true
|
||||||
};
|
};
|
||||||
|
|
||||||
dcRouter = new DcRouter({ emailConfig });
|
dcRouter = new DcRouter({ emailConfig, cacheConfig: { enabled: false } });
|
||||||
|
|
||||||
// Access the private method to generate routes
|
// Access the private method to generate routes
|
||||||
const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig);
|
const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig);
|
||||||
@@ -119,7 +121,8 @@ tap.test('email socket handler should handle different ports correctly', async (
|
|||||||
domains: ['test.local'],
|
domains: ['test.local'],
|
||||||
routes: [],
|
routes: [],
|
||||||
useSocketHandler: true
|
useSocketHandler: true
|
||||||
}
|
},
|
||||||
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
await dcRouter.start();
|
await dcRouter.start();
|
||||||
@@ -145,7 +148,8 @@ tap.test('email server handleSocket method should work', async () => {
|
|||||||
domains: ['test.local'],
|
domains: ['test.local'],
|
||||||
routes: [],
|
routes: [],
|
||||||
useSocketHandler: true
|
useSocketHandler: true
|
||||||
}
|
},
|
||||||
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
await dcRouter.start();
|
await dcRouter.start();
|
||||||
@@ -182,7 +186,8 @@ tap.test('should not create SMTP servers when useSocketHandler is true', async (
|
|||||||
domains: ['test.local'],
|
domains: ['test.local'],
|
||||||
routes: [],
|
routes: [],
|
||||||
useSocketHandler: true
|
useSocketHandler: true
|
||||||
}
|
},
|
||||||
|
cacheConfig: { enabled: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
await dcRouter.start();
|
await dcRouter.start();
|
||||||
@@ -209,7 +214,7 @@ tap.test('TLS handling should differ between ports', async () => {
|
|||||||
useSocketHandler: false // Use traditional mode to check TLS config
|
useSocketHandler: false // Use traditional mode to check TLS config
|
||||||
};
|
};
|
||||||
|
|
||||||
dcRouter = new DcRouter({ emailConfig });
|
dcRouter = new DcRouter({ emailConfig, cacheConfig: { enabled: false } });
|
||||||
|
|
||||||
const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig);
|
const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '4.0.0',
|
version: '4.1.1',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
39
ts/cache/classes.cache.cleaner.ts
vendored
39
ts/cache/classes.cache.cleaner.ts
vendored
@@ -98,24 +98,20 @@ export class CacheCleaner {
|
|||||||
const results: { collection: string; deleted: number }[] = [];
|
const results: { collection: string; deleted: number }[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clean CachedEmail documents
|
// Clean each collection using smartdata's getInstances + delete pattern
|
||||||
const emailsDeleted = await this.cleanCollection(CachedEmail, now);
|
const emailsDeleted = await this.cleanExpiredDocuments(CachedEmail, now);
|
||||||
results.push({ collection: 'CachedEmail', deleted: emailsDeleted });
|
results.push({ collection: 'CachedEmail', deleted: emailsDeleted });
|
||||||
|
|
||||||
// Clean CachedIPReputation documents
|
const ipReputationDeleted = await this.cleanExpiredDocuments(CachedIPReputation, now);
|
||||||
const ipReputationDeleted = await this.cleanCollection(CachedIPReputation, now);
|
|
||||||
results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted });
|
results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted });
|
||||||
|
|
||||||
// Clean CachedBounce documents
|
const bouncesDeleted = await this.cleanExpiredDocuments(CachedBounce, now);
|
||||||
const bouncesDeleted = await this.cleanCollection(CachedBounce, now);
|
|
||||||
results.push({ collection: 'CachedBounce', deleted: bouncesDeleted });
|
results.push({ collection: 'CachedBounce', deleted: bouncesDeleted });
|
||||||
|
|
||||||
// Clean CachedSuppression documents (but not permanent ones)
|
const suppressionDeleted = await this.cleanExpiredDocuments(CachedSuppression, now);
|
||||||
const suppressionDeleted = await this.cleanCollection(CachedSuppression, now);
|
|
||||||
results.push({ collection: 'CachedSuppression', deleted: suppressionDeleted });
|
results.push({ collection: 'CachedSuppression', deleted: suppressionDeleted });
|
||||||
|
|
||||||
// Clean CachedDKIMKey documents
|
const dkimDeleted = await this.cleanExpiredDocuments(CachedDKIMKey, now);
|
||||||
const dkimDeleted = await this.cleanCollection(CachedDKIMKey, now);
|
|
||||||
results.push({ collection: 'CachedDKIMKey', deleted: dkimDeleted });
|
results.push({ collection: 'CachedDKIMKey', deleted: dkimDeleted });
|
||||||
|
|
||||||
// Log results
|
// Log results
|
||||||
@@ -137,17 +133,30 @@ export class CacheCleaner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean expired documents from a specific collection
|
* Clean expired documents from a specific collection using smartdata API
|
||||||
*/
|
*/
|
||||||
private async cleanCollection<T>(
|
private async cleanExpiredDocuments<T extends { delete: () => Promise<void> }>(
|
||||||
documentClass: { deleteMany: (filter: any) => Promise<any> },
|
documentClass: { getInstances: (filter: any) => Promise<T[]> },
|
||||||
now: Date
|
now: Date
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const result = await documentClass.deleteMany({
|
// Find all expired documents
|
||||||
|
const expiredDocs = await documentClass.getInstances({
|
||||||
expiresAt: { $lt: now },
|
expiresAt: { $lt: now },
|
||||||
});
|
});
|
||||||
return result?.deletedCount || 0;
|
|
||||||
|
// Delete each expired document
|
||||||
|
let deletedCount = 0;
|
||||||
|
for (const doc of expiredDocs) {
|
||||||
|
try {
|
||||||
|
await doc.delete();
|
||||||
|
deletedCount++;
|
||||||
|
} catch (deleteError) {
|
||||||
|
logger.log('warn', `Failed to delete expired document: ${deleteError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('error', `Error cleaning collection: ${error.message}`);
|
logger.log('error', `Error cleaning collection: ${error.message}`);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
9
ts/cache/classes.cached.document.ts
vendored
9
ts/cache/classes.cached.document.ts
vendored
@@ -7,24 +7,27 @@ import * as plugins from '../plugins.js';
|
|||||||
* - Automatic timestamps (createdAt, lastAccessedAt)
|
* - Automatic timestamps (createdAt, lastAccessedAt)
|
||||||
* - TTL/expiration support (expiresAt)
|
* - TTL/expiration support (expiresAt)
|
||||||
* - Helper methods for TTL management
|
* - Helper methods for TTL management
|
||||||
|
*
|
||||||
|
* NOTE: Subclasses MUST add @svDb() decorators to createdAt, expiresAt, and lastAccessedAt
|
||||||
|
* since decorators on abstract classes don't propagate correctly.
|
||||||
*/
|
*/
|
||||||
export abstract class CachedDocument<T extends CachedDocument<T>> extends plugins.smartdata.SmartDataDbDoc<T, T> {
|
export abstract class CachedDocument<T extends CachedDocument<T>> extends plugins.smartdata.SmartDataDbDoc<T, T> {
|
||||||
/**
|
/**
|
||||||
* Timestamp when the document was created
|
* Timestamp when the document was created
|
||||||
|
* NOTE: Subclasses must add @svDb() decorator
|
||||||
*/
|
*/
|
||||||
@plugins.smartdata.svDb()
|
|
||||||
public createdAt: Date = new Date();
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp when the document expires and should be cleaned up
|
* Timestamp when the document expires and should be cleaned up
|
||||||
|
* NOTE: Subclasses must add @svDb() decorator
|
||||||
*/
|
*/
|
||||||
@plugins.smartdata.svDb()
|
|
||||||
public expiresAt: Date;
|
public expiresAt: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of last access (for LRU-style eviction if needed)
|
* Timestamp of last access (for LRU-style eviction if needed)
|
||||||
|
* NOTE: Subclasses must add @svDb() decorator
|
||||||
*/
|
*/
|
||||||
@plugins.smartdata.svDb()
|
|
||||||
public lastAccessedAt: Date = new Date();
|
public lastAccessedAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
16
ts/cache/classes.cachedb.ts
vendored
16
ts/cache/classes.cachedb.ts
vendored
@@ -69,19 +69,21 @@ export class CacheDb {
|
|||||||
|
|
||||||
// Create LocalTsmDb instance
|
// Create LocalTsmDb instance
|
||||||
this.localTsmDb = new plugins.smartmongo.LocalTsmDb({
|
this.localTsmDb = new plugins.smartmongo.LocalTsmDb({
|
||||||
dbDir: this.options.storagePath,
|
folderPath: this.options.storagePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start LocalTsmDb and get connection URI
|
// Start LocalTsmDb and get connection info
|
||||||
await this.localTsmDb.start();
|
const connectionInfo = await this.localTsmDb.start();
|
||||||
const mongoDescriptor = this.localTsmDb.mongoDescriptor;
|
|
||||||
|
|
||||||
if (this.options.debug) {
|
if (this.options.debug) {
|
||||||
logger.log('debug', `LocalTsmDb started with descriptor: ${JSON.stringify(mongoDescriptor)}`);
|
logger.log('debug', `LocalTsmDb started with URI: ${connectionInfo.connectionUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize smartdata with the connection
|
// Initialize smartdata with the connection URI
|
||||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(mongoDescriptor);
|
this.smartdataDb = new plugins.smartdata.SmartdataDb({
|
||||||
|
mongoDbUrl: connectionInfo.connectionUri,
|
||||||
|
mongoDbName: this.options.dbName,
|
||||||
|
});
|
||||||
await this.smartdataDb.init();
|
await this.smartdataDb.init();
|
||||||
|
|
||||||
this.isStarted = true;
|
this.isStarted = true;
|
||||||
|
|||||||
10
ts/cache/documents/classes.cached.bounce.ts
vendored
10
ts/cache/documents/classes.cached.bounce.ts
vendored
@@ -33,6 +33,16 @@ export type TBounceCategory =
|
|||||||
*/
|
*/
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
export class CachedBounce extends CachedDocument<CachedBounce> {
|
export class CachedBounce extends CachedDocument<CachedBounce> {
|
||||||
|
// TTL fields from base class (decorators required on concrete class)
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastAccessedAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique identifier for this bounce record
|
* Unique identifier for this bounce record
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
ts/cache/documents/classes.cached.dkim.ts
vendored
10
ts/cache/documents/classes.cached.dkim.ts
vendored
@@ -15,6 +15,16 @@ const getDb = () => CacheDb.getInstance().getDb();
|
|||||||
*/
|
*/
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
export class CachedDKIMKey extends CachedDocument<CachedDKIMKey> {
|
export class CachedDKIMKey extends CachedDocument<CachedDKIMKey> {
|
||||||
|
// TTL fields from base class (decorators required on concrete class)
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_90);
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastAccessedAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composite key: domain:selector
|
* Composite key: domain:selector
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
ts/cache/documents/classes.cached.email.ts
vendored
10
ts/cache/documents/classes.cached.email.ts
vendored
@@ -20,6 +20,16 @@ const getDb = () => CacheDb.getInstance().getDb();
|
|||||||
*/
|
*/
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
export class CachedEmail extends CachedDocument<CachedEmail> {
|
export class CachedEmail extends CachedDocument<CachedEmail> {
|
||||||
|
// TTL fields from base class (decorators required on concrete class)
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastAccessedAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique identifier for this email
|
* Unique identifier for this email
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ export interface IIPReputationData {
|
|||||||
*/
|
*/
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
export class CachedIPReputation extends CachedDocument<CachedIPReputation> {
|
export class CachedIPReputation extends CachedDocument<CachedIPReputation> {
|
||||||
|
// TTL fields from base class (decorators required on concrete class)
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public expiresAt: Date = new Date(Date.now() + TTL.HOURS_24);
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastAccessedAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IP address (unique identifier)
|
* IP address (unique identifier)
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
ts/cache/documents/classes.cached.suppression.ts
vendored
10
ts/cache/documents/classes.cached.suppression.ts
vendored
@@ -27,6 +27,16 @@ export type TSuppressionReason =
|
|||||||
*/
|
*/
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
export class CachedSuppression extends CachedDocument<CachedSuppression> {
|
export class CachedSuppression extends CachedDocument<CachedSuppression> {
|
||||||
|
// TTL fields from base class (decorators required on concrete class)
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastAccessedAt: Date = new Date();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Email address to suppress (unique identifier)
|
* Email address to suppress (unique identifier)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { logger } from './logger.js';
|
|||||||
import { configureEmailStorage, configureEmailServer } from './mail/delivery/index.js';
|
import { configureEmailStorage, configureEmailServer } from './mail/delivery/index.js';
|
||||||
// Import storage manager
|
// Import storage manager
|
||||||
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
||||||
|
// Import cache system
|
||||||
|
import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
|
||||||
|
|
||||||
import { OpsServer } from './opsserver/index.js';
|
import { OpsServer } from './opsserver/index.js';
|
||||||
import { MetricsManager } from './monitoring/index.js';
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
@@ -111,6 +113,36 @@ export interface IDcRouterOptions {
|
|||||||
/** Storage configuration */
|
/** Storage configuration */
|
||||||
storage?: IStorageConfig;
|
storage?: IStorageConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache database configuration using smartdata and LocalTsmDb
|
||||||
|
* Provides persistent caching for emails, IP reputation, bounces, etc.
|
||||||
|
*/
|
||||||
|
cacheConfig?: {
|
||||||
|
/** Enable cache database (default: true) */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Storage path for TsmDB data (default: /etc/dcrouter/tsmdb) */
|
||||||
|
storagePath?: string;
|
||||||
|
/** Database name (default: dcrouter) */
|
||||||
|
dbName?: string;
|
||||||
|
/** Default TTL in days for cached items (default: 30) */
|
||||||
|
defaultTTLDays?: number;
|
||||||
|
/** Cleanup interval in hours (default: 1) */
|
||||||
|
cleanupIntervalHours?: number;
|
||||||
|
/** TTL configuration per data type (in days) */
|
||||||
|
ttlConfig?: {
|
||||||
|
/** Email cache TTL (default: 30 days) */
|
||||||
|
emails?: number;
|
||||||
|
/** IP reputation cache TTL (default: 1 day) */
|
||||||
|
ipReputation?: number;
|
||||||
|
/** Bounce records TTL (default: 30 days) */
|
||||||
|
bounces?: number;
|
||||||
|
/** DKIM keys TTL (default: 90 days) */
|
||||||
|
dkimKeys?: number;
|
||||||
|
/** Suppression list TTL (default: 30 days, can be permanent) */
|
||||||
|
suppression?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RADIUS server configuration for network authentication
|
* RADIUS server configuration for network authentication
|
||||||
* Enables MAC Authentication Bypass (MAB) and VLAN assignment
|
* Enables MAC Authentication Bypass (MAB) and VLAN assignment
|
||||||
@@ -144,6 +176,10 @@ export class DcRouter {
|
|||||||
public opsServer: OpsServer;
|
public opsServer: OpsServer;
|
||||||
public metricsManager?: MetricsManager;
|
public metricsManager?: MetricsManager;
|
||||||
|
|
||||||
|
// Cache system (smartdata + LocalTsmDb)
|
||||||
|
public cacheDb?: CacheDb;
|
||||||
|
public cacheCleaner?: CacheCleaner;
|
||||||
|
|
||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
@@ -170,6 +206,11 @@ export class DcRouter {
|
|||||||
await this.opsServer.start();
|
await this.opsServer.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Initialize cache database if enabled (default: enabled)
|
||||||
|
if (this.options.cacheConfig?.enabled !== false) {
|
||||||
|
await this.setupCacheDb();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize MetricsManager
|
// Initialize MetricsManager
|
||||||
this.metricsManager = new MetricsManager(this);
|
this.metricsManager = new MetricsManager(this);
|
||||||
await this.metricsManager.start();
|
await this.metricsManager.start();
|
||||||
@@ -291,9 +332,45 @@ export class DcRouter {
|
|||||||
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
|
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache database summary
|
||||||
|
if (this.cacheDb) {
|
||||||
|
console.log('\n🗄️ Cache Database (smartdata + LocalTsmDb):');
|
||||||
|
console.log(` ├─ Storage: ${this.cacheDb.getStoragePath()}`);
|
||||||
|
console.log(` ├─ Database: ${this.cacheDb.getDbName()}`);
|
||||||
|
console.log(` └─ Cleaner: ${this.cacheCleaner?.isActive() ? 'Active' : 'Inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('\n✅ All services are running\n');
|
console.log('\n✅ All services are running\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the cache database (smartdata + LocalTsmDb)
|
||||||
|
*/
|
||||||
|
private async setupCacheDb(): Promise<void> {
|
||||||
|
logger.log('info', 'Setting up CacheDb...');
|
||||||
|
|
||||||
|
const cacheConfig = this.options.cacheConfig || {};
|
||||||
|
|
||||||
|
// Initialize CacheDb singleton
|
||||||
|
this.cacheDb = CacheDb.getInstance({
|
||||||
|
storagePath: cacheConfig.storagePath || '/etc/dcrouter/tsmdb',
|
||||||
|
dbName: cacheConfig.dbName || 'dcrouter',
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.cacheDb.start();
|
||||||
|
|
||||||
|
// Start the cache cleaner
|
||||||
|
const cleanupIntervalMs = (cacheConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000;
|
||||||
|
this.cacheCleaner = new CacheCleaner(this.cacheDb, {
|
||||||
|
intervalMs: cleanupIntervalMs,
|
||||||
|
verbose: false,
|
||||||
|
});
|
||||||
|
this.cacheCleaner.start();
|
||||||
|
|
||||||
|
logger.log('info', `CacheDb initialized at ${this.cacheDb.getStoragePath()}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up SmartProxy with direct configuration and automatic email routes
|
* Set up SmartProxy with direct configuration and automatic email routes
|
||||||
*/
|
*/
|
||||||
@@ -604,6 +681,9 @@ export class DcRouter {
|
|||||||
try {
|
try {
|
||||||
// Stop all services in parallel for faster shutdown
|
// Stop all services in parallel for faster shutdown
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
// Stop cache cleaner if running
|
||||||
|
this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop metrics manager if running
|
// Stop metrics manager if running
|
||||||
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
||||||
|
|
||||||
@@ -624,6 +704,11 @@ export class DcRouter {
|
|||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Stop cache database after other services (they may need it during shutdown)
|
||||||
|
if (this.cacheDb) {
|
||||||
|
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
|
||||||
|
}
|
||||||
|
|
||||||
console.log('All DcRouter services stopped');
|
console.log('All DcRouter services stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during DcRouter shutdown:', error);
|
console.error('Error during DcRouter shutdown:', error);
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export class MetricsManager {
|
|||||||
return this.metricsCache.get('serverStats', async () => {
|
return this.metricsCache.get('serverStats', async () => {
|
||||||
const smartMetricsData = await this.smartMetrics.getMetrics();
|
const smartMetricsData = await this.smartMetrics.getMetrics();
|
||||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStatistics() : null;
|
const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '4.0.0',
|
version: '4.1.1',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
|||||||
Reference in New Issue
Block a user