@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rljson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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.
package/README.blog.md ADDED
@@ -0,0 +1,11 @@
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
+ # Blog
10
+
11
+ Add latest posts at the end.
@@ -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/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 |