@rljson/network 0.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/LICENSE +21 -0
- package/README.architecture.md +290 -0
- package/README.blog.md +11 -0
- package/README.contributors.md +32 -0
- package/README.md +24 -0
- package/README.public.md +426 -0
- package/README.trouble.md +23 -0
- package/dist/README.architecture.md +290 -0
- package/dist/README.blog.md +11 -0
- package/dist/README.contributors.md +32 -0
- package/dist/README.md +24 -0
- package/dist/README.public.md +426 -0
- package/dist/README.trouble.md +23 -0
- package/dist/election/hub-election.d.ts +28 -0
- package/dist/example.d.ts +1 -0
- package/dist/identity/node-identity.d.ts +52 -0
- package/dist/index.d.ts +29 -0
- package/dist/layers/broadcast-layer.d.ts +132 -0
- package/dist/layers/cloud-layer.d.ts +156 -0
- package/dist/layers/discovery-layer.d.ts +45 -0
- package/dist/layers/manual-layer.d.ts +56 -0
- package/dist/layers/static-layer.d.ts +61 -0
- package/dist/network-manager.d.ts +149 -0
- package/dist/network.js +1691 -0
- package/dist/peer-table.d.ts +79 -0
- package/dist/probing/peer-prober.d.ts +21 -0
- package/dist/probing/probe-scheduler.d.ts +119 -0
- package/dist/src/example.ts +105 -0
- package/dist/types/network-config.d.ts +63 -0
- package/dist/types/network-events.d.ts +33 -0
- package/dist/types/network-topology.d.ts +29 -0
- package/dist/types/node-info.d.ts +19 -0
- package/dist/types/peer-probe.d.ts +15 -0
- package/package.json +51 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# Architecture
|
|
10
|
+
|
|
11
|
+
## Design Principle: Zero RLJSON Knowledge
|
|
12
|
+
|
|
13
|
+
The network package knows **nothing** about Io, Bs, Db, trees, hashes, or
|
|
14
|
+
sync. It only knows about nodes, peers, and topology. This makes it reusable
|
|
15
|
+
for any application that needs self-organizing star topology.
|
|
16
|
+
|
|
17
|
+
- Use `domain` (network grouping), **never** `treeKey` (RLJSON concept)
|
|
18
|
+
- No imports from `@rljson/io`, `@rljson/bs`, `@rljson/db`, `@rljson/server`
|
|
19
|
+
- The bridge between network → RLJSON lives in `@rljson/server`, not here
|
|
20
|
+
|
|
21
|
+
## Module Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
├── types/
|
|
26
|
+
│ ├── node-info.ts // NodeId, NodeInfo — core identity type
|
|
27
|
+
│ ├── peer-probe.ts // PeerProbe — probe result
|
|
28
|
+
│ ├── network-topology.ts // NetworkTopology, NodeRole, FormedBy
|
|
29
|
+
│ ├── network-config.ts // Config interfaces for all discovery layers
|
|
30
|
+
│ └── network-events.ts // Event types emitted by NetworkManager
|
|
31
|
+
├── identity/
|
|
32
|
+
│ └── node-identity.ts // Persistent UUID, hostname, IP discovery
|
|
33
|
+
├── election/
|
|
34
|
+
│ └── hub-election.ts // Deterministic hub election algorithm
|
|
35
|
+
├── probing/
|
|
36
|
+
│ ├── peer-prober.ts // Real TCP connect probe via node:net
|
|
37
|
+
│ └── probe-scheduler.ts // Periodic probing + change detection
|
|
38
|
+
├── layers/
|
|
39
|
+
│ ├── discovery-layer.ts // DiscoveryLayer interface — contract for all layers
|
|
40
|
+
│ ├── broadcast-layer.ts // BroadcastLayer — UDP broadcast discovery (Try 1)
|
|
41
|
+
│ ├── manual-layer.ts // ManualLayer — always-present override
|
|
42
|
+
│ └── static-layer.ts // StaticLayer — hardcoded hub fallback (Try 3)
|
|
43
|
+
├── peer-table.ts // PeerTable — merged view of peers from all layers
|
|
44
|
+
├── network-manager.ts // NetworkManager — central orchestrator
|
|
45
|
+
├── example.ts // Runnable example
|
|
46
|
+
└── index.ts // Public API exports
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Fallback Cascade
|
|
50
|
+
|
|
51
|
+
Discovery layers are tried in order of autonomy:
|
|
52
|
+
|
|
53
|
+
| Position | Layer | Required? | Description |
|
|
54
|
+
| ------------ | ------------- | ------------------ | ---------------------------------------- |
|
|
55
|
+
| **Try 1** | UDP Broadcast | **always on** | Zero-config LAN discovery. Primary path. |
|
|
56
|
+
| **Try 2** | Cloud Service | **optional** | Cross-network fallback. |
|
|
57
|
+
| **Try 3** | Static Config | **optional** | Hardcoded `hubAddress`. Last resort. |
|
|
58
|
+
| **Override** | Manual / UI | **always present** | Human escape hatch. No config entry. |
|
|
59
|
+
|
|
60
|
+
## Hub Election Rules
|
|
61
|
+
|
|
62
|
+
1. **Incumbent advantage**: If a hub exists and is reachable, keep it.
|
|
63
|
+
2. **Earliest `startedAt`**: If no incumbent, the node started first wins.
|
|
64
|
+
3. **Tiebreaker**: Lexicographic `nodeId` comparison.
|
|
65
|
+
|
|
66
|
+
### Election Implementation
|
|
67
|
+
|
|
68
|
+
`electHub()` in `src/election/hub-election.ts` is a **pure function** — no
|
|
69
|
+
I/O, no side effects, fully deterministic:
|
|
70
|
+
|
|
71
|
+
- **Input**: candidates (`NodeInfo[]`), probes (`PeerProbe[]`), current hub, self
|
|
72
|
+
- **Output**: `ElectionResult` with `hubId` and `reason`
|
|
73
|
+
- Self is **always** considered reachable (no need for self-probe)
|
|
74
|
+
- Returns `{ hubId: null, reason: 'no-candidates' }` when no reachable nodes
|
|
75
|
+
- Reasons: `'incumbent'`, `'earliest-start'`, `'tiebreaker'`, `'no-candidates'`
|
|
76
|
+
|
|
77
|
+
## Probing Design
|
|
78
|
+
|
|
79
|
+
### PeerProber
|
|
80
|
+
|
|
81
|
+
`probePeer()` in `src/probing/peer-prober.ts` uses real TCP connect via
|
|
82
|
+
`node:net`:
|
|
83
|
+
|
|
84
|
+
- `connect({ host, port, timeout })` — non-blocking socket connect
|
|
85
|
+
- Never rejects — always resolves with a `PeerProbe` result
|
|
86
|
+
- `reachable: true` on `'connect'`, `false` on `'timeout'` or `'error'`
|
|
87
|
+
- Latency measured with `performance.now()` (sub-millisecond precision)
|
|
88
|
+
- Socket is destroyed immediately after result (no keep-alive)
|
|
89
|
+
|
|
90
|
+
### ProbeScheduler
|
|
91
|
+
|
|
92
|
+
`ProbeScheduler` in `src/probing/probe-scheduler.ts` manages periodic
|
|
93
|
+
probing of all known peers:
|
|
94
|
+
|
|
95
|
+
- **Injectable probe function** (`ProbeFn`) for Tier-1 mock tests
|
|
96
|
+
- **Real TCP** by default (uses `probePeer`)
|
|
97
|
+
- **Self-exclusion at probe time** — filters self in `_runCycle()`, not
|
|
98
|
+
`setPeers()`, because `setPeers()` may be called before `start()` sets
|
|
99
|
+
the self ID
|
|
100
|
+
- **Change detection**: tracks `_wasReachable` state per peer, emits
|
|
101
|
+
`'peer-unreachable'` and `'peer-reachable'` only on state transitions
|
|
102
|
+
- **Flap dampening**: a peer must fail `failThreshold` consecutive probes
|
|
103
|
+
(default: 3) before being declared unreachable. A single success resets
|
|
104
|
+
the counter immediately. This prevents flapping on transient network glitches.
|
|
105
|
+
- **No false alerts**: first probe sets baseline, subsequent probes detect
|
|
106
|
+
changes
|
|
107
|
+
- `runOnce()` — manual single cycle for deterministic test control
|
|
108
|
+
- Events: `'probes-updated'`, `'peer-unreachable'`, `'peer-reachable'`
|
|
109
|
+
|
|
110
|
+
## Testing Strategy (3-Tier)
|
|
111
|
+
|
|
112
|
+
| Tier | Scope | What's real | Used for |
|
|
113
|
+
| ------ | ---------------------- | ------------------ | ------------------------------------ |
|
|
114
|
+
| Tier 1 | Unit (mock probes) | Logic + state only | Election, scheduling, event emission |
|
|
115
|
+
| Tier 2 | Real TCP (localhost) | Actual socket I/O | Probing, latency, connection refused |
|
|
116
|
+
| Tier 3 | Multi-process (future) | Full network stack | True distributed scenarios |
|
|
117
|
+
|
|
118
|
+
- Tier 1 uses `ProbeFn` injection for deterministic, fast tests
|
|
119
|
+
- Tier 2 uses `node:net.createServer()` on localhost with random ports
|
|
120
|
+
- All tiers run in the same `pnpm test` suite
|
|
121
|
+
|
|
122
|
+
## NodeIdentity Design
|
|
123
|
+
|
|
124
|
+
`NodeIdentity` uses dependency injection for full testability:
|
|
125
|
+
|
|
126
|
+
- `NodeIdentityDeps` interface — all OS/fs functions injectable
|
|
127
|
+
- `defaultNodeIdentityDeps()` — real Node.js implementations
|
|
128
|
+
- `NodeIdentity.create(options)` — async factory, loads or generates UUID
|
|
129
|
+
- UUID persisted at `<identityDir>/<domain>/node-id`
|
|
130
|
+
- Same machine + same domain = same identity across restarts
|
|
131
|
+
|
|
132
|
+
## DiscoveryLayer Contract
|
|
133
|
+
|
|
134
|
+
All discovery mechanisms implement the `DiscoveryLayer` interface:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
start(identity) → Promise<boolean> // Returns false if layer can't operate
|
|
138
|
+
stop() → Promise<void> // Clean up resources
|
|
139
|
+
isActive() → boolean // Whether currently running
|
|
140
|
+
getPeers() → NodeInfo[] // Currently known peers
|
|
141
|
+
getAssignedHub() → string | null // Hub dictated by this layer
|
|
142
|
+
on/off → event subscription // peer-discovered, peer-lost, hub-assigned
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### ManualLayer
|
|
146
|
+
|
|
147
|
+
- **Always present**, cannot be disabled
|
|
148
|
+
- Does **not** discover peers — only overrides hub assignment
|
|
149
|
+
- `assignHub(nodeId)` / `clearOverride()` API
|
|
150
|
+
- Clearing returns control to the automatic cascade
|
|
151
|
+
|
|
152
|
+
### BroadcastLayer (Try 1 — Primary Discovery)
|
|
153
|
+
|
|
154
|
+
UDP broadcast discovery for zero-config LAN peer detection:
|
|
155
|
+
|
|
156
|
+
- **Self-test on startup**: sends a broadcast and waits for loopback reception;
|
|
157
|
+
if the self-test times out, broadcast is unavailable and the cascade falls
|
|
158
|
+
through to the next layer (cloud or static)
|
|
159
|
+
- **Periodic broadcasting** at `intervalMs` (default: 5000 ms)
|
|
160
|
+
- **Domain filtering**: ignores packets from nodes in a different `domain`
|
|
161
|
+
- **Self-packet detection**: loopback packets are silently dropped (never
|
|
162
|
+
added to the peer table)
|
|
163
|
+
- **Peer timeout**: peers that haven't been heard from within `timeoutMs`
|
|
164
|
+
(default: 15000 ms) are removed and `peer-lost` is emitted
|
|
165
|
+
- **Does NOT assign hub** — `getAssignedHub()` always returns `null`.
|
|
166
|
+
Hub is elected by the NetworkManager via `electHub()` when broadcast
|
|
167
|
+
peers are available (`formedBy: 'broadcast'`)
|
|
168
|
+
- **Testable via `UdpSocket` interface** — injectable socket factory
|
|
169
|
+
(`CreateUdpSocket`) for deterministic mock-based testing with
|
|
170
|
+
`MockUdpHub` / `MockUdpSocket`
|
|
171
|
+
|
|
172
|
+
Configuration (`BroadcastConfig`):
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
{
|
|
176
|
+
enabled: boolean; // default: true
|
|
177
|
+
port: number; // default: 41234
|
|
178
|
+
intervalMs?: number; // default: 5000
|
|
179
|
+
timeoutMs?: number; // default: 15000
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### StaticLayer
|
|
184
|
+
|
|
185
|
+
- Last resort fallback (Try 3)
|
|
186
|
+
- Reads `hubAddress` from config (`"ip:port"`)
|
|
187
|
+
- Creates a **synthetic peer** with deterministic nodeId `static-hub-<address>`
|
|
188
|
+
- Returns `false` from `start()` if no `hubAddress` configured
|
|
189
|
+
- Emits `peer-discovered` + `hub-assigned` on start, `peer-lost` on stop
|
|
190
|
+
|
|
191
|
+
## PeerTable Design
|
|
192
|
+
|
|
193
|
+
Merged view of all peers from all discovery layers:
|
|
194
|
+
|
|
195
|
+
- **Deduplication by nodeId** — same peer from multiple layers appears once
|
|
196
|
+
- **Per-layer tracking** via `_layerPeers` map for correct removal semantics
|
|
197
|
+
- `peer-joined` fires only when a genuinely **new** peer is first seen
|
|
198
|
+
- `peer-left` fires only when **all** layers have lost the peer
|
|
199
|
+
- `setSelfId()` excludes own node from the peer table
|
|
200
|
+
- `attachLayer()` imports existing peers + subscribes to future events
|
|
201
|
+
|
|
202
|
+
## NetworkManager Design
|
|
203
|
+
|
|
204
|
+
Central orchestrator — the main public API:
|
|
205
|
+
|
|
206
|
+
- Creates `NodeIdentity` on start (respects `identityDir` from config)
|
|
207
|
+
- Starts ManualLayer, BroadcastLayer, CloudLayer, and StaticLayer in cascade order
|
|
208
|
+
- Creates and manages `ProbeScheduler` for reachability checking
|
|
209
|
+
- Uses `PeerTable` for merged peer tracking
|
|
210
|
+
- Applies **cascade logic** via `_computeHub()`:
|
|
211
|
+
1. Manual override wins
|
|
212
|
+
2. Election via probes (if probes available → `electHub()`)
|
|
213
|
+
- `formedBy: 'broadcast'` when broadcast layer is active with peers
|
|
214
|
+
- `formedBy: 'election'` otherwise
|
|
215
|
+
3. Cloud assignment (cloud dictates hub — has the full picture)
|
|
216
|
+
4. Static config fallback
|
|
217
|
+
5. No result → `unassigned`
|
|
218
|
+
- Accepts `NetworkManagerOptions` with injectable `probeFn`,
|
|
219
|
+
`broadcastDeps`, and `cloudDeps` for testing
|
|
220
|
+
- Emits events: `topology-changed`, `role-changed`, `hub-changed`,
|
|
221
|
+
`peer-joined`, `peer-left`
|
|
222
|
+
- Continuous re-evaluation: any peer/hub change or probe update triggers
|
|
223
|
+
`_recomputeTopology()`
|
|
224
|
+
- `getProbeScheduler()` provides public access for advanced usage
|
|
225
|
+
|
|
226
|
+
## Known Limitations (Future Work)
|
|
227
|
+
|
|
228
|
+
The current election system is designed for **LAN office sync** (2–10 nodes)
|
|
229
|
+
and works well in that scope. The following distributed-systems gaps are
|
|
230
|
+
documented for future epics:
|
|
231
|
+
|
|
232
|
+
### Split-Brain — No Consensus Protocol
|
|
233
|
+
|
|
234
|
+
Each node runs its own `electHub()` independently. In a network partition,
|
|
235
|
+
two isolated groups may each elect a different hub. There is no Raft, Paxos,
|
|
236
|
+
or gossip protocol to reach agreement.
|
|
237
|
+
|
|
238
|
+
**Mitigation**: On LAN, partitions are rare. When the partition heals, probes
|
|
239
|
+
converge and nodes re-elect the same hub (incumbent advantage + earliest
|
|
240
|
+
`startedAt`).
|
|
241
|
+
|
|
242
|
+
**Future**: A gossip protocol (Epic 5+) or Raft-based consensus could
|
|
243
|
+
guarantee a single hub across partitions.
|
|
244
|
+
|
|
245
|
+
### No Quorum
|
|
246
|
+
|
|
247
|
+
Election doesn't require a majority of nodes to agree. A single node
|
|
248
|
+
in isolation will elect itself as hub. This is by design for small LANs
|
|
249
|
+
but may be undesirable in larger deployments.
|
|
250
|
+
|
|
251
|
+
**Future**: Add optional `quorum: true` config that requires `>50%` of
|
|
252
|
+
known peers to be reachable before accepting an election result.
|
|
253
|
+
|
|
254
|
+
### Probe Staleness Window
|
|
255
|
+
|
|
256
|
+
The default probe interval is 10 seconds. During this window, a hub can
|
|
257
|
+
go down without being detected. This is acceptable for file sync but not
|
|
258
|
+
for real-time systems.
|
|
259
|
+
|
|
260
|
+
**Mitigation**: `intervalMs` is configurable. For faster detection, reduce
|
|
261
|
+
the interval (at the cost of higher network traffic).
|
|
262
|
+
|
|
263
|
+
**Future**: Hub heartbeat / lease mechanism — the hub actively sends
|
|
264
|
+
keepalives; clients consider the hub down if heartbeat stops.
|
|
265
|
+
|
|
266
|
+
### No Handover Protocol
|
|
267
|
+
|
|
268
|
+
When a hub is replaced, there is no graceful handover of state (e.g.,
|
|
269
|
+
pending syncs, in-flight messages). Clients simply reconnect to the new hub.
|
|
270
|
+
|
|
271
|
+
**Future**: Handover protocol lives in `@rljson/server`, not in the
|
|
272
|
+
network layer. The network layer only signals topology changes; the
|
|
273
|
+
application layer (`@rljson/server`) must handle state transfer.
|
|
274
|
+
|
|
275
|
+
### No Hub Lease / Heartbeat
|
|
276
|
+
|
|
277
|
+
There is no mechanism for the hub to "renew" its leadership. If the hub
|
|
278
|
+
stops responding but probe timing creates a race, two nodes might briefly
|
|
279
|
+
disagree about who is hub.
|
|
280
|
+
|
|
281
|
+
**Mitigation**: Flap dampening (3 consecutive failures required) prevents
|
|
282
|
+
transient disagreements. Incumbent advantage prevents unnecessary re-elections.
|
|
283
|
+
|
|
284
|
+
**Future**: A time-limited hub lease (e.g., 30 seconds) with active renewal
|
|
285
|
+
would provide stronger guarantees.
|
|
286
|
+
|
|
287
|
+
## Layer 0 — No Dependencies
|
|
288
|
+
|
|
289
|
+
This package has **zero** `@rljson/*` dependencies. It sits at Layer 0
|
|
290
|
+
alongside `@rljson/rljson`, meaning it can be published independently.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# Contributors Guide
|
|
10
|
+
|
|
11
|
+
- [Prepare](#prepare)
|
|
12
|
+
- [Develop](#develop)
|
|
13
|
+
- [Administrate](#administrate)
|
|
14
|
+
- [Fast Coding](#fast-coding)
|
|
15
|
+
|
|
16
|
+
## Prepare
|
|
17
|
+
|
|
18
|
+
Read [prepare.md](doc/prepare.md)
|
|
19
|
+
|
|
20
|
+
<!-- ........................................................................-->
|
|
21
|
+
|
|
22
|
+
## Develop
|
|
23
|
+
|
|
24
|
+
Read [develop.md](doc/develop.md)
|
|
25
|
+
|
|
26
|
+
## Administrate
|
|
27
|
+
|
|
28
|
+
Read [create-new-repo.md](doc/create-new-repo.md)
|
|
29
|
+
|
|
30
|
+
## Fast Coding
|
|
31
|
+
|
|
32
|
+
Read [fast-coding-guide.md](doc/fast-coding-guide.md)
|
package/dist/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @rljson/network
|
|
10
|
+
|
|
11
|
+
## Users
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
| ------------------------------------ | --------------------------- |
|
|
15
|
+
| [README.public.md](README.public.md) | Install and use the package |
|
|
16
|
+
|
|
17
|
+
## Contributors
|
|
18
|
+
|
|
19
|
+
| File | Purpose |
|
|
20
|
+
| ------------------------------------------------ | ----------------------------- |
|
|
21
|
+
| [README.contributors.md](README.contributors.md) | Run, debug, build and publish |
|
|
22
|
+
| [README.architecture.md](README.architecture.md) | Software architecture guide |
|
|
23
|
+
| [README.trouble.md](README.trouble.md) | Errors & solutions |
|
|
24
|
+
| [README.blog.md](README.blog.md) | Blog |
|