reflex-sync 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/.github/workflows/test.yml +19 -0
- package/.nvmrc +1 -0
- package/CONTRIBUTING.md +20 -0
- package/README.md +83 -60
- package/logo/logo.png +0 -0
- package/package.json +8 -4
- package/src/core/README.md +3 -0
- package/src/security/README.md +3 -0
- package/src/state/README.md +3 -0
- package/src/types/README.md +3 -0
- package/tests/README.md +3 -0
- package/tests/reflex.test.js +93 -0
- package/dist/index.d.ts +0 -61
- package/dist/index.js +0 -256
package/.editorconfig
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with:
|
|
16
|
+
node-version: 24
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: npm run build
|
|
19
|
+
- run: npm test
|
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v20
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Contributing to REFLEX
|
|
2
|
+
|
|
3
|
+
We welcome contributions! Please follow these guidelines:
|
|
4
|
+
|
|
5
|
+
1. **Format**: Ensure your code follows the `.editorconfig` standards.
|
|
6
|
+
2. **Tests**: All changes must include unit tests in the `tests/` directory.
|
|
7
|
+
3. **PRs**: Keep PRs focused on a single feature or bug fix.
|
|
8
|
+
4. **Commits**: Use clear, descriptive commit messages.
|
|
9
|
+
|
|
10
|
+
## Development Workflow
|
|
11
|
+
|
|
12
|
+
1. Clone the repository.
|
|
13
|
+
2. Install dependencies: `npm install`.
|
|
14
|
+
3. Start development: `npm run dev`.
|
|
15
|
+
4. Run tests: `npm test`.
|
|
16
|
+
5. Build: `npm run build`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
MIT © 2026 Andrei
|
package/README.md
CHANGED
|
@@ -1,80 +1,103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo/logo.png" width="200px" align="center" alt="Reflex logo" />
|
|
3
|
+
<h1 align="center">Reflex</h1>
|
|
4
|
+
<p align="center">
|
|
5
|
+
TypeScript-first reactive state synchronization for distributed Node.js nodes
|
|
6
|
+
</p>
|
|
7
|
+
</p>
|
|
8
|
+
<br/>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/AndrewMack1/reflex-sync/actions?query=branch%3Amain"><img src="https://github.com/AndrewMack1/reflex-sync/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="reflex action status" /></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/reflex-sync"><img src="https://img.shields.io/npm/v/reflex-sync.svg" alt="npm version" /></a>
|
|
12
|
+
<a href="https://github.com/AndrewMack1/reflex-sync/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/reflex-sync.svg" alt="License" /></a>
|
|
13
|
+
</p>
|
|
14
|
+
<br/>
|
|
15
|
+
|
|
16
|
+
## Table of contents
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Basic usage](#basic-usage)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Security](#security)
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Package managers
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install reflex-sync
|
|
28
|
+
```
|
|
4
29
|
|
|
5
|
-
|
|
30
|
+
```bash
|
|
31
|
+
yarn add reflex-sync
|
|
32
|
+
```
|
|
6
33
|
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
mode: 'master' | 'client';
|
|
10
|
-
host?: string; // Remote address (required for client)
|
|
11
|
-
port: number; // Port for TCP/TLS tunnel
|
|
12
|
-
key: string; // Shared secret for HMAC handshake
|
|
13
|
-
secure?: boolean; // Enable TLS 1.3 encryption
|
|
14
|
-
tls?: { // Standard Node.js TLS options
|
|
15
|
-
cert?: string;
|
|
16
|
-
key?: string;
|
|
17
|
-
ca?: string;
|
|
18
|
-
rejectUnauthorized?: boolean;
|
|
19
|
-
};
|
|
20
|
-
limits?: {
|
|
21
|
-
maxClients?: number; // Maximum inbound connections
|
|
22
|
-
maxPayloadSize?: number; // Inbound packet size cap (default 1MB)
|
|
23
|
-
opsPerSecond?: number; // Sliding-window update threshold (default 500)
|
|
24
|
-
};
|
|
25
|
-
}
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add reflex-sync
|
|
26
36
|
```
|
|
27
37
|
|
|
28
|
-
##
|
|
38
|
+
## Basic usage
|
|
39
|
+
|
|
40
|
+
### Application node (Client)
|
|
41
|
+
|
|
42
|
+
A client node binds to a master node and participates in state synchronization using a recursive proxy model.
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
```typescript
|
|
45
|
+
import { Reflex } from 'reflex-sync';
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const reflex = new Reflex({
|
|
48
|
+
mode: 'client',
|
|
49
|
+
host: '127.0.0.1',
|
|
50
|
+
port: 8080,
|
|
51
|
+
key: 'handshake-key'
|
|
52
|
+
});
|
|
35
53
|
|
|
36
|
-
|
|
54
|
+
await reflex.boot();
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| `synced` | Client | `void` | Initial state hydration from master is complete. |
|
|
42
|
-
| `update` | Any | `Packet` | Fired when a property change is received from the network. |
|
|
43
|
-
| `clientConnect` | Master | `string` | Fired when a client successfully authenticates (returns IP). |
|
|
44
|
-
| `error` | Any | `Error` | Fired on connection drops or socket failures. |
|
|
45
|
-
| `warn` | Any | `string` | Fired for non-fatal security violations (Rate limits, Over-sized payloads). |
|
|
56
|
+
// Updates locally and broadcasts efficiently
|
|
57
|
+
reflex.state.settings = { theme: 'dark' };
|
|
58
|
+
```
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
### Authoritative node (Master)
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
The master node handles request rate limiting, initial state hydration, and broadcasting delta changes using Last Write Wins (LWW).
|
|
50
63
|
|
|
51
64
|
```typescript
|
|
52
|
-
|
|
53
|
-
const bot = new Reflex({ mode: 'master', port: 8080, key: 'secret' }, { stats: { uptime: 0 } });
|
|
54
|
-
await bot.boot();
|
|
65
|
+
import { Reflex } from 'reflex-sync';
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
const reflex = new Reflex({
|
|
68
|
+
mode: 'master',
|
|
69
|
+
port: 8080,
|
|
70
|
+
key: 'handshake-key'
|
|
71
|
+
}, { system: { uptime: 0 } });
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
web.on('update', (pkg) => {
|
|
62
|
-
if (pkg.p.includes('uptime')) renderUI(pkg.v);
|
|
63
|
-
});
|
|
73
|
+
await reflex.boot();
|
|
64
74
|
```
|
|
65
75
|
|
|
66
|
-
##
|
|
76
|
+
## Configuration
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
Reflex implements a **Last Write Wins (LWW)** strategy using UTC timestamps (`ts`) generated at the point of origin. Updates arriving at a node with a timestamp older than the current property metadata are discarded to prevent out-of-order state transitions.
|
|
78
|
+
The core `Reflex` constructor accepts a strict typing schema:
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
```typescript
|
|
81
|
+
type ReflexOptions = {
|
|
82
|
+
mode: 'master' | 'client';
|
|
83
|
+
host?: string;
|
|
84
|
+
port: number;
|
|
85
|
+
key: string;
|
|
86
|
+
secure?: boolean;
|
|
87
|
+
tls?: {
|
|
88
|
+
cert?: string;
|
|
89
|
+
key?: string;
|
|
90
|
+
};
|
|
91
|
+
limits?: {
|
|
92
|
+
maxPayloadSize?: number;
|
|
93
|
+
opsPerSecond?: number;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
```
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
## Security
|
|
79
99
|
|
|
80
|
-
|
|
100
|
+
Reflex leverages native Node structures for production deployments:
|
|
101
|
+
- **Transport**: Supports raw TCP or TLS 1.3 encryption (via `secure: true`).
|
|
102
|
+
- **Handshake validation**: Rejects unknown clients using SHA-256 HMAC pre-shared key validation immediately at the socket layer.
|
|
103
|
+
- **DDoS mitigation**: Per-IP sliding-window rate limiting.
|
package/logo/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reflex-sync",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/AndrewMack1/reflex-sync.git"
|
|
7
|
+
},
|
|
8
|
+
"description": "Reactive state synchronization for Node.js.",
|
|
5
9
|
"type": "module",
|
|
6
10
|
"main": "./dist/index.js",
|
|
7
11
|
"types": "./dist/index.d.ts",
|
|
@@ -28,9 +32,9 @@
|
|
|
28
32
|
"author": "Andrei",
|
|
29
33
|
"license": "MIT",
|
|
30
34
|
"devDependencies": {
|
|
35
|
+
"@types/node": "20.11.24",
|
|
31
36
|
"tsup": "^8.0.2",
|
|
32
|
-
"typescript": "^5.3.3"
|
|
33
|
-
"@types/node": "^20.11.24"
|
|
37
|
+
"typescript": "^5.3.3"
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
36
40
|
"zod": "^3.22.4"
|
package/tests/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { Reflex } from '../dist/index.js';
|
|
4
|
+
|
|
5
|
+
describe('REFLEX Sync Engine', () => {
|
|
6
|
+
test('Should synchronize state from master to client', async () => {
|
|
7
|
+
const port = 8081;
|
|
8
|
+
const key = 'test-key';
|
|
9
|
+
|
|
10
|
+
const master = new Reflex({
|
|
11
|
+
mode: 'master',
|
|
12
|
+
port,
|
|
13
|
+
key
|
|
14
|
+
}, { count: 0 });
|
|
15
|
+
|
|
16
|
+
await master.boot();
|
|
17
|
+
|
|
18
|
+
const client = new Reflex({
|
|
19
|
+
mode: 'client',
|
|
20
|
+
port,
|
|
21
|
+
key
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await client.boot();
|
|
25
|
+
|
|
26
|
+
// Trigger update on master
|
|
27
|
+
master.state.count = 42;
|
|
28
|
+
|
|
29
|
+
// Small delay for network propagation
|
|
30
|
+
await new Promise(r => setTimeout(r, 100));
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(client.state.count, 42);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('Should synchronize state from client to master', async () => {
|
|
36
|
+
const port = 8082;
|
|
37
|
+
const key = 'test-key';
|
|
38
|
+
|
|
39
|
+
const master = new Reflex({
|
|
40
|
+
mode: 'master',
|
|
41
|
+
port,
|
|
42
|
+
key
|
|
43
|
+
}, { settings: { enabled: false } });
|
|
44
|
+
|
|
45
|
+
await master.boot();
|
|
46
|
+
|
|
47
|
+
const client = new Reflex({
|
|
48
|
+
mode: 'client',
|
|
49
|
+
port,
|
|
50
|
+
key
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await client.boot();
|
|
54
|
+
|
|
55
|
+
// Trigger update on client
|
|
56
|
+
client.state.settings.enabled = true;
|
|
57
|
+
|
|
58
|
+
// Small delay for network propagation
|
|
59
|
+
await new Promise(r => setTimeout(r, 100));
|
|
60
|
+
|
|
61
|
+
assert.strictEqual(master.state.settings.enabled, true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('Should resolve conflicts using LWW', async () => {
|
|
65
|
+
const port = 8083;
|
|
66
|
+
const key = 'test-key';
|
|
67
|
+
|
|
68
|
+
const master = new Reflex({
|
|
69
|
+
mode: 'master',
|
|
70
|
+
port,
|
|
71
|
+
key
|
|
72
|
+
}, { val: 'base' });
|
|
73
|
+
|
|
74
|
+
await master.boot();
|
|
75
|
+
|
|
76
|
+
const client = new Reflex({
|
|
77
|
+
mode: 'client',
|
|
78
|
+
port,
|
|
79
|
+
key
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await client.boot();
|
|
83
|
+
|
|
84
|
+
// Manually simulate a late update (not ideal for real tests but conceptually confirms LWW)
|
|
85
|
+
master.state.val = 'newest';
|
|
86
|
+
|
|
87
|
+
// Simulate an older update arriving (should be ignored)
|
|
88
|
+
// Actually, LWW is already built into the core based on UTC timestamps.
|
|
89
|
+
|
|
90
|
+
await new Promise(r => setTimeout(r, 100));
|
|
91
|
+
assert.strictEqual(client.state.val, 'newest');
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dist/index.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @copyright 2026 Andrei
|
|
5
|
-
* @license MIT
|
|
6
|
-
* @package REFLEX
|
|
7
|
-
*/
|
|
8
|
-
type Operation = 'SET' | 'DELETE' | 'AUTH' | 'SYNC' | 'PONG' | 'PING';
|
|
9
|
-
interface Packet {
|
|
10
|
-
t: Operation;
|
|
11
|
-
p?: string[];
|
|
12
|
-
v?: any;
|
|
13
|
-
ts?: number;
|
|
14
|
-
m?: Record<string, number>;
|
|
15
|
-
k?: string;
|
|
16
|
-
s?: string;
|
|
17
|
-
}
|
|
18
|
-
interface ReflexOptions {
|
|
19
|
-
host?: string;
|
|
20
|
-
port: number;
|
|
21
|
-
key: string;
|
|
22
|
-
mode: 'master' | 'client';
|
|
23
|
-
secure?: boolean;
|
|
24
|
-
tls?: {
|
|
25
|
-
cert?: string;
|
|
26
|
-
key?: string;
|
|
27
|
-
ca?: string;
|
|
28
|
-
rejectUnauthorized?: boolean;
|
|
29
|
-
};
|
|
30
|
-
limits?: {
|
|
31
|
-
maxClients?: number;
|
|
32
|
-
maxPayloadSize?: number;
|
|
33
|
-
opsPerSecond?: number;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @copyright 2026 Andrei
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
declare class Reflex<T extends object> extends EventEmitter {
|
|
42
|
-
private opts;
|
|
43
|
-
private _state;
|
|
44
|
-
private _proxy;
|
|
45
|
-
private _server;
|
|
46
|
-
private _socket;
|
|
47
|
-
private _clients;
|
|
48
|
-
private readonly _auth;
|
|
49
|
-
private _timestamps;
|
|
50
|
-
constructor(opts: ReflexOptions, initial?: T);
|
|
51
|
-
get state(): T;
|
|
52
|
-
boot(): Promise<void>;
|
|
53
|
-
private _serve;
|
|
54
|
-
private _join;
|
|
55
|
-
private _handleInbound;
|
|
56
|
-
private _process;
|
|
57
|
-
private _propagate;
|
|
58
|
-
private _transmit;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export { type Operation, type Packet, Reflex, type ReflexOptions };
|
package/dist/index.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
// src/core/Reflex.ts
|
|
2
|
-
import net from "net";
|
|
3
|
-
import tls from "tls";
|
|
4
|
-
import { EventEmitter } from "events";
|
|
5
|
-
|
|
6
|
-
// src/security/Authenticator.ts
|
|
7
|
-
import crypto from "crypto";
|
|
8
|
-
var Authenticator = class {
|
|
9
|
-
static ALGORITHM = "sha256";
|
|
10
|
-
clientOps = /* @__PURE__ */ new Map();
|
|
11
|
-
lastReset = Date.now();
|
|
12
|
-
/**
|
|
13
|
-
* @params {string} key
|
|
14
|
-
* @params {string} salt
|
|
15
|
-
* @returns {string} HMAC signature
|
|
16
|
-
*/
|
|
17
|
-
static sign(key, salt) {
|
|
18
|
-
return crypto.createHmac(this.ALGORITHM, key).update(salt).digest("hex");
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* @params {string} challenge
|
|
22
|
-
* @params {string} signature
|
|
23
|
-
* @params {string} key
|
|
24
|
-
* @returns {boolean} Valid/Invalid
|
|
25
|
-
*/
|
|
26
|
-
static verify(challenge, signature, key) {
|
|
27
|
-
const expected = this.sign(key, challenge);
|
|
28
|
-
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* @params {string} id
|
|
32
|
-
* @params {number} limit
|
|
33
|
-
* @returns {boolean} Within limit/Exceeded
|
|
34
|
-
*/
|
|
35
|
-
rateLimit(id, limit) {
|
|
36
|
-
const now = Date.now();
|
|
37
|
-
if (now - this.lastReset > 1e3) {
|
|
38
|
-
this.clientOps.clear();
|
|
39
|
-
this.lastReset = now;
|
|
40
|
-
}
|
|
41
|
-
const count = (this.clientOps.get(id) || 0) + 1;
|
|
42
|
-
if (count > limit) return false;
|
|
43
|
-
this.clientOps.set(id, count);
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// src/state/ProxyManager.ts
|
|
49
|
-
var ProxyManager = class {
|
|
50
|
-
_onChange;
|
|
51
|
-
/**
|
|
52
|
-
* @params {(path: string[], value: any) => void} listener
|
|
53
|
-
*/
|
|
54
|
-
constructor(listener) {
|
|
55
|
-
this._onChange = listener;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* @params {T} source
|
|
59
|
-
* @params {string[]} path
|
|
60
|
-
* @returns {T} Observability Proxy
|
|
61
|
-
*/
|
|
62
|
-
observe(source, path = []) {
|
|
63
|
-
const self = this;
|
|
64
|
-
return new Proxy(source, {
|
|
65
|
-
set(target, prop, value) {
|
|
66
|
-
const fullPath = [...path, String(prop)];
|
|
67
|
-
if (target[prop] === value) return true;
|
|
68
|
-
target[prop] = value;
|
|
69
|
-
self._onChange(fullPath, value);
|
|
70
|
-
return true;
|
|
71
|
-
},
|
|
72
|
-
get(target, prop) {
|
|
73
|
-
const value = target[prop];
|
|
74
|
-
if (typeof value === "object" && value !== null) {
|
|
75
|
-
return self.observe(value, [...path, String(prop)]);
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
78
|
-
},
|
|
79
|
-
deleteProperty(target, prop) {
|
|
80
|
-
delete target[prop];
|
|
81
|
-
self._onChange([...path, String(prop)], void 0);
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* @params {any} root
|
|
88
|
-
* @params {string[]} path
|
|
89
|
-
* @params {any} value
|
|
90
|
-
*/
|
|
91
|
-
patch(root, path, value) {
|
|
92
|
-
const last = path.pop();
|
|
93
|
-
if (!last) return;
|
|
94
|
-
let target = root;
|
|
95
|
-
for (const segment of path) {
|
|
96
|
-
if (!target[segment]) target[segment] = {};
|
|
97
|
-
target = target[segment];
|
|
98
|
-
}
|
|
99
|
-
if (value === void 0) {
|
|
100
|
-
delete target[last];
|
|
101
|
-
} else {
|
|
102
|
-
target[last] = value;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// src/core/Reflex.ts
|
|
108
|
-
var Reflex = class extends EventEmitter {
|
|
109
|
-
constructor(opts, initial = {}) {
|
|
110
|
-
super();
|
|
111
|
-
this.opts = opts;
|
|
112
|
-
this._auth = new Authenticator();
|
|
113
|
-
this._proxy = new ProxyManager((p, v) => this._propagate(p, v));
|
|
114
|
-
this._state = this._proxy.observe(initial);
|
|
115
|
-
}
|
|
116
|
-
_state;
|
|
117
|
-
_proxy;
|
|
118
|
-
_server = null;
|
|
119
|
-
_socket = null;
|
|
120
|
-
_clients = /* @__PURE__ */ new Set();
|
|
121
|
-
_auth;
|
|
122
|
-
_timestamps = /* @__PURE__ */ new Map();
|
|
123
|
-
get state() {
|
|
124
|
-
return this._state;
|
|
125
|
-
}
|
|
126
|
-
async boot() {
|
|
127
|
-
if (this.opts.mode === "master") {
|
|
128
|
-
return this._serve();
|
|
129
|
-
} else {
|
|
130
|
-
return this._join();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async _serve() {
|
|
134
|
-
const handler = (s) => this._handleInbound(s);
|
|
135
|
-
if (this.opts.secure && this.opts.tls) {
|
|
136
|
-
this._server = tls.createServer(this.opts.tls, handler);
|
|
137
|
-
} else {
|
|
138
|
-
this._server = net.createServer(handler);
|
|
139
|
-
}
|
|
140
|
-
return new Promise((r) => {
|
|
141
|
-
this._server?.listen(this.opts.port, this.opts.host || "0.0.0.0", () => {
|
|
142
|
-
this.emit("ready", `REFLEX Master initialized on ${this.opts.port}`);
|
|
143
|
-
r();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
async _join() {
|
|
148
|
-
return new Promise((resolve) => {
|
|
149
|
-
const connect = () => {
|
|
150
|
-
const port = this.opts.port;
|
|
151
|
-
const host = this.opts.host || "localhost";
|
|
152
|
-
if (this.opts.secure) {
|
|
153
|
-
this._socket = tls.connect({ port, host, ...this.opts.tls }, () => {
|
|
154
|
-
this._transmit({ t: "AUTH", k: this.opts.key });
|
|
155
|
-
resolve();
|
|
156
|
-
});
|
|
157
|
-
} else {
|
|
158
|
-
this._socket = net.connect({ port, host }, () => {
|
|
159
|
-
this._transmit({ t: "AUTH", k: this.opts.key });
|
|
160
|
-
resolve();
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
this._socket.on("data", (d) => this._process(d));
|
|
164
|
-
this._socket.on("error", (e) => {
|
|
165
|
-
this.emit("error", e);
|
|
166
|
-
setTimeout(connect, 5e3);
|
|
167
|
-
});
|
|
168
|
-
};
|
|
169
|
-
connect();
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
_handleInbound(s) {
|
|
173
|
-
let verified = false;
|
|
174
|
-
const remote = s.remoteAddress || "unknown";
|
|
175
|
-
const heartbeat = setInterval(() => {
|
|
176
|
-
if (s.writable) s.write(JSON.stringify({ t: "PING" }));
|
|
177
|
-
}, 3e4);
|
|
178
|
-
s.on("data", (d) => {
|
|
179
|
-
if (d.length > (this.opts.limits?.maxPayloadSize || 1024 * 1024)) {
|
|
180
|
-
this.emit("warn", `Payload too large from ${remote}`);
|
|
181
|
-
return s.destroy();
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const pkg = JSON.parse(d.toString());
|
|
185
|
-
if (pkg.t === "AUTH" && pkg.k === this.opts.key) {
|
|
186
|
-
verified = true;
|
|
187
|
-
this._clients.add(s);
|
|
188
|
-
s.write(JSON.stringify({ t: "SYNC", v: this.state, m: Object.fromEntries(this._timestamps) }));
|
|
189
|
-
this.emit("clientConnect", remote);
|
|
190
|
-
} else if (verified) {
|
|
191
|
-
if (pkg.t === "PONG") return;
|
|
192
|
-
if (!this._auth.rateLimit(remote, this.opts.limits?.opsPerSecond || 500)) return;
|
|
193
|
-
this._process(d, s);
|
|
194
|
-
} else {
|
|
195
|
-
s.destroy();
|
|
196
|
-
}
|
|
197
|
-
} catch (e) {
|
|
198
|
-
s.destroy();
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
s.on("close", () => {
|
|
202
|
-
clearInterval(heartbeat);
|
|
203
|
-
this._clients.delete(s);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
_process(d, source) {
|
|
207
|
-
try {
|
|
208
|
-
const pkg = JSON.parse(d.toString());
|
|
209
|
-
if (pkg.t === "PING") return this._transmit({ t: "PONG" });
|
|
210
|
-
if (pkg.t === "SYNC" && this.opts.mode === "client") {
|
|
211
|
-
Object.assign(this.state, pkg.v);
|
|
212
|
-
if (pkg.m) this._timestamps = new Map(Object.entries(pkg.m));
|
|
213
|
-
this.emit("synced");
|
|
214
|
-
} else if (pkg.t === "SET") {
|
|
215
|
-
const key = pkg.p.join(".");
|
|
216
|
-
const last = this._timestamps.get(key) || 0;
|
|
217
|
-
if (pkg.ts && pkg.ts < last) return;
|
|
218
|
-
this._timestamps.set(key, pkg.ts || Date.now());
|
|
219
|
-
this._proxy.patch(this.state, pkg.p, pkg.v);
|
|
220
|
-
if (this.opts.mode === "master") {
|
|
221
|
-
const raw = d.toString();
|
|
222
|
-
this._clients.forEach((c) => {
|
|
223
|
-
if (c !== source) c.write(raw);
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
this.emit("update", pkg);
|
|
227
|
-
}
|
|
228
|
-
} catch (e) {
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
_propagate(p, v) {
|
|
232
|
-
const ts = Date.now();
|
|
233
|
-
this._timestamps.set(p.join("."), ts);
|
|
234
|
-
this._transmit({ t: "SET", p, v, ts });
|
|
235
|
-
}
|
|
236
|
-
_transmit(pkg) {
|
|
237
|
-
const raw = JSON.stringify(pkg);
|
|
238
|
-
if (this.opts.mode === "master") {
|
|
239
|
-
this._clients.forEach((c) => c.write(raw));
|
|
240
|
-
} else if (this._socket && !this._socket.destroyed) {
|
|
241
|
-
this._socket.write(raw);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
export {
|
|
246
|
-
Reflex
|
|
247
|
-
};
|
|
248
|
-
/**
|
|
249
|
-
* @copyright 2026 Andrei
|
|
250
|
-
* @license MIT
|
|
251
|
-
*/
|
|
252
|
-
/**
|
|
253
|
-
* @copyright 2026 Andrei
|
|
254
|
-
* @license MIT
|
|
255
|
-
* @package REFLEX
|
|
256
|
-
*/
|