@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
package/README.public.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
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
|
+
Self-organizing network topology for the RLJSON ecosystem. Handles peer
|
|
12
|
+
discovery, hub election, and topology formation — enabling nodes to
|
|
13
|
+
automatically form star-topology networks without pre-assigned roles or
|
|
14
|
+
hardcoded IPs.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @rljson/network
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Key Concepts
|
|
23
|
+
|
|
24
|
+
| Concept | Description |
|
|
25
|
+
| ---------------- | --------------------------------------------------------------- |
|
|
26
|
+
| **Domain** | Groups nodes that should discover each other (not a DNS domain) |
|
|
27
|
+
| **Hub** | Central node elected automatically — all others are clients |
|
|
28
|
+
| **Discovery** | Fallback cascade: Broadcast → Cloud → Static + Manual override |
|
|
29
|
+
| **Hub Election** | Incumbent advantage + earliest `startedAt` timestamp wins |
|
|
30
|
+
|
|
31
|
+
## Types
|
|
32
|
+
|
|
33
|
+
### NodeInfo
|
|
34
|
+
|
|
35
|
+
Describes a node in the network:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import type { NodeInfo } from '@rljson/network';
|
|
39
|
+
|
|
40
|
+
const node: NodeInfo = {
|
|
41
|
+
nodeId: 'a1b2c3d4-...', // Persistent UUID
|
|
42
|
+
hostname: 'WORKSTATION-7', // os.hostname()
|
|
43
|
+
localIps: ['192.168.1.42'], // Non-internal IPv4 addresses
|
|
44
|
+
domain: 'office-sync', // Network group
|
|
45
|
+
port: 3000, // Listen port when hub
|
|
46
|
+
startedAt: 1741123456789, // Startup timestamp
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### PeerProbe
|
|
51
|
+
|
|
52
|
+
Result of probing a peer's reachability:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import type { PeerProbe } from '@rljson/network';
|
|
56
|
+
|
|
57
|
+
const probe: PeerProbe = {
|
|
58
|
+
fromNodeId: 'node-a',
|
|
59
|
+
toNodeId: 'node-b',
|
|
60
|
+
reachable: true,
|
|
61
|
+
latencyMs: 12,
|
|
62
|
+
measuredAt: 1741123456800,
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### NetworkTopology
|
|
67
|
+
|
|
68
|
+
Snapshot of the current network layout:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import type { NetworkTopology } from '@rljson/network';
|
|
72
|
+
|
|
73
|
+
const topo: NetworkTopology = {
|
|
74
|
+
domain: 'office-sync',
|
|
75
|
+
hubNodeId: 'a1b2c3d4-...',
|
|
76
|
+
hubAddress: '192.168.1.42:3000',
|
|
77
|
+
formedBy: 'broadcast', // 'broadcast' | 'cloud' | 'election' | 'static' | 'manual'
|
|
78
|
+
formedAt: 1741123456800,
|
|
79
|
+
nodes: { /* NodeInfo by nodeId */ },
|
|
80
|
+
probes: [ /* PeerProbe[] */ ],
|
|
81
|
+
myRole: 'hub', // 'hub' | 'client' | 'unassigned'
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### NetworkConfig
|
|
86
|
+
|
|
87
|
+
Configuration for the discovery layers:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { defaultNetworkConfig } from '@rljson/network';
|
|
91
|
+
|
|
92
|
+
// Quick start — broadcast enabled, cloud/static off
|
|
93
|
+
const config = defaultNetworkConfig('office-sync', 3000);
|
|
94
|
+
|
|
95
|
+
// Full configuration
|
|
96
|
+
import type { NetworkConfig } from '@rljson/network';
|
|
97
|
+
|
|
98
|
+
const fullConfig: NetworkConfig = {
|
|
99
|
+
domain: 'office-sync',
|
|
100
|
+
port: 3000,
|
|
101
|
+
identityDir: '/opt/myapp/identity', // Default: ~/.rljson-network/
|
|
102
|
+
broadcast: { enabled: true, port: 41234, intervalMs: 5000 },
|
|
103
|
+
cloud: { enabled: true, endpoint: 'https://cloud.example.com', apiKey: '...' },
|
|
104
|
+
static: { hubAddress: '192.168.1.100:3000' },
|
|
105
|
+
probing: { enabled: true, intervalMs: 10000, timeoutMs: 2000 },
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Network Events
|
|
110
|
+
|
|
111
|
+
Events emitted by the network manager:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import type { NetworkEventMap } from '@rljson/network';
|
|
115
|
+
import { networkEventNames } from '@rljson/network';
|
|
116
|
+
|
|
117
|
+
// Event names: 'topology-changed', 'role-changed', 'hub-changed',
|
|
118
|
+
// 'peer-joined', 'peer-left'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## NodeIdentity
|
|
122
|
+
|
|
123
|
+
Persistent node identity — generates a UUID on first run, reads the same
|
|
124
|
+
UUID on subsequent runs (same machine = same identity):
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { NodeIdentity } from '@rljson/network';
|
|
128
|
+
|
|
129
|
+
const identity = await NodeIdentity.create({
|
|
130
|
+
domain: 'office-sync',
|
|
131
|
+
port: 3000,
|
|
132
|
+
// identityDir: '/custom/path', // Default: ~/.rljson-network/
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log(identity.nodeId); // Persistent UUID
|
|
136
|
+
console.log(identity.hostname); // Machine hostname
|
|
137
|
+
console.log(identity.localIps); // ['192.168.1.42']
|
|
138
|
+
|
|
139
|
+
const info = identity.toNodeInfo(); // Plain NodeInfo object
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Discovery Layers
|
|
143
|
+
|
|
144
|
+
All layers implement the `DiscoveryLayer` interface:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import type { DiscoveryLayer } from '@rljson/network';
|
|
148
|
+
// Methods: start(), stop(), isActive(), getPeers(), getAssignedHub(), on(), off()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### ManualLayer
|
|
152
|
+
|
|
153
|
+
Always-present manual override. Cannot be disabled:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { ManualLayer } from '@rljson/network';
|
|
157
|
+
|
|
158
|
+
const manual = new ManualLayer();
|
|
159
|
+
await manual.start(identity);
|
|
160
|
+
|
|
161
|
+
manual.assignHub('specific-node-id'); // Force a hub
|
|
162
|
+
manual.clearOverride(); // Return to cascade
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### BroadcastLayer (Try 1)
|
|
166
|
+
|
|
167
|
+
UDP broadcast discovery — zero-config LAN peer detection:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { BroadcastLayer, defaultCreateUdpSocket } from '@rljson/network';
|
|
171
|
+
import type { BroadcastConfig, BroadcastLayerDeps } from '@rljson/network';
|
|
172
|
+
|
|
173
|
+
const config: BroadcastConfig = {
|
|
174
|
+
enabled: true,
|
|
175
|
+
port: 41234,
|
|
176
|
+
intervalMs: 5000, // How often to broadcast
|
|
177
|
+
timeoutMs: 15000, // Remove peer after silence
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const layer = new BroadcastLayer(config);
|
|
181
|
+
const started = await layer.start(identity);
|
|
182
|
+
// started = true if UDP broadcast is available (self-test passed)
|
|
183
|
+
// started = false if broadcast blocked, disabled, or bind failed
|
|
184
|
+
|
|
185
|
+
layer.on('peer-discovered', (peer) => console.log('Found:', peer.nodeId));
|
|
186
|
+
layer.on('peer-lost', (nodeId) => console.log('Lost:', nodeId));
|
|
187
|
+
|
|
188
|
+
console.log(layer.getPeers()); // Currently discovered peers
|
|
189
|
+
console.log(layer.getAssignedHub()); // Always null (broadcast doesn't assign)
|
|
190
|
+
|
|
191
|
+
await layer.stop();
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
For testing, inject a mock socket factory:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import type { UdpSocket, CreateUdpSocket } from '@rljson/network';
|
|
198
|
+
|
|
199
|
+
const deps: BroadcastLayerDeps = {
|
|
200
|
+
createSocket: myMockSocketFactory,
|
|
201
|
+
selfTestTimeoutMs: 50,
|
|
202
|
+
};
|
|
203
|
+
const layer = new BroadcastLayer(config, deps);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### CloudLayer (Try 2)
|
|
207
|
+
|
|
208
|
+
REST-based cloud discovery fallback. The cloud service has the full picture
|
|
209
|
+
and **dictates** the hub (unlike broadcast, which uses local election):
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { CloudLayer, defaultCreateCloudHttpClient } from '@rljson/network';
|
|
213
|
+
import type { CloudConfig, CloudLayerDeps, CloudHttpClient } from '@rljson/network';
|
|
214
|
+
|
|
215
|
+
const config: CloudConfig = {
|
|
216
|
+
enabled: true,
|
|
217
|
+
endpoint: 'https://cloud.example.com',
|
|
218
|
+
apiKey: 'my-api-key', // Optional Bearer token
|
|
219
|
+
pollIntervalMs: 30000, // How often to poll (default: 30000)
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const layer = new CloudLayer(config);
|
|
223
|
+
const started = await layer.start(identity);
|
|
224
|
+
// started = true if enabled + endpoint present + registration succeeded
|
|
225
|
+
// started = false if disabled or no endpoint
|
|
226
|
+
|
|
227
|
+
layer.on('peer-discovered', (peer) => console.log('Cloud peer:', peer.nodeId));
|
|
228
|
+
layer.on('hub-assigned', (hubId) => console.log('Cloud assigned hub:', hubId));
|
|
229
|
+
|
|
230
|
+
console.log(layer.getPeers()); // Peers from cloud
|
|
231
|
+
console.log(layer.getAssignedHub()); // Hub dictated by cloud, or null
|
|
232
|
+
|
|
233
|
+
// Report local probe results to cloud (cloud uses these for hub decisions)
|
|
234
|
+
await layer.reportProbes(probes);
|
|
235
|
+
|
|
236
|
+
await layer.stop();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For testing, inject a mock HTTP client:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import type { CloudHttpClient, CloudLayerDeps } from '@rljson/network';
|
|
243
|
+
|
|
244
|
+
const mockClient: CloudHttpClient = {
|
|
245
|
+
register: async () => ({ peers: [], assignedHub: null }),
|
|
246
|
+
poll: async () => ({ peers: [], assignedHub: null }),
|
|
247
|
+
reportProbes: async () => {},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const deps: CloudLayerDeps = { createHttpClient: () => mockClient };
|
|
251
|
+
const layer = new CloudLayer(config, deps);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### StaticLayer
|
|
255
|
+
|
|
256
|
+
Last-resort fallback — reads a hardcoded hub address from config:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { StaticLayer } from '@rljson/network';
|
|
260
|
+
|
|
261
|
+
const staticLayer = new StaticLayer({ hubAddress: '192.168.1.100:3000' });
|
|
262
|
+
const started = await staticLayer.start(identity);
|
|
263
|
+
// started = true (has config), creates synthetic peer for hub
|
|
264
|
+
|
|
265
|
+
const noConfig = new StaticLayer();
|
|
266
|
+
const started2 = await noConfig.start(identity);
|
|
267
|
+
// started2 = false (no hubAddress configured)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## PeerTable
|
|
271
|
+
|
|
272
|
+
Merged view of all peers from all discovery layers. Deduplicates by nodeId:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { PeerTable } from '@rljson/network';
|
|
276
|
+
|
|
277
|
+
const table = new PeerTable();
|
|
278
|
+
table.setSelfId(identity.nodeId);
|
|
279
|
+
|
|
280
|
+
table.attachLayer(staticLayer); // Import peers + subscribe to events
|
|
281
|
+
table.attachLayer(manualLayer);
|
|
282
|
+
|
|
283
|
+
table.on('peer-joined', (peer) => console.log('New peer:', peer.nodeId));
|
|
284
|
+
table.on('peer-left', (nodeId) => console.log('Lost peer:', nodeId));
|
|
285
|
+
|
|
286
|
+
console.log(table.getPeers()); // All known peers
|
|
287
|
+
console.log(table.size); // Number of peers
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## NetworkManager
|
|
291
|
+
|
|
292
|
+
Central orchestrator — starts layers, merges peer tables, applies cascade
|
|
293
|
+
logic, and emits topology events:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { NetworkManager, defaultNetworkConfig } from '@rljson/network';
|
|
297
|
+
|
|
298
|
+
const config = {
|
|
299
|
+
...defaultNetworkConfig('office-sync', 3000),
|
|
300
|
+
static: { hubAddress: '192.168.1.100:3000' },
|
|
301
|
+
};
|
|
302
|
+
const manager = new NetworkManager(config);
|
|
303
|
+
|
|
304
|
+
manager.on('topology-changed', (e) => {
|
|
305
|
+
console.log('Topology:', e.topology.myRole, e.topology.formedBy);
|
|
306
|
+
});
|
|
307
|
+
manager.on('role-changed', (e) => {
|
|
308
|
+
console.log(`Role: ${e.previous} → ${e.current}`);
|
|
309
|
+
});
|
|
310
|
+
manager.on('hub-changed', (e) => {
|
|
311
|
+
console.log(`Hub: ${e.previousHub} → ${e.currentHub}`);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await manager.start();
|
|
315
|
+
|
|
316
|
+
const topology = manager.getTopology();
|
|
317
|
+
// { myRole: 'client', formedBy: 'static', hubAddress: '192.168.1.100:3000', ... }
|
|
318
|
+
|
|
319
|
+
// Manual override supersedes cascade
|
|
320
|
+
manager.assignHub('custom-hub-id');
|
|
321
|
+
// Now formedBy: 'manual'
|
|
322
|
+
|
|
323
|
+
// Revert to cascade
|
|
324
|
+
manager.clearOverride();
|
|
325
|
+
// Back to formedBy: 'static'
|
|
326
|
+
|
|
327
|
+
await manager.stop();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Hub Decision Cascade
|
|
331
|
+
|
|
332
|
+
The `NetworkManager` evaluates hub assignment in this order:
|
|
333
|
+
|
|
334
|
+
1. **Manual override** → human knows best
|
|
335
|
+
2. **Election via probing** → probes determine reachable peers, election picks hub
|
|
336
|
+
- `formedBy: 'broadcast'` when broadcast layer is active with peers
|
|
337
|
+
- `formedBy: 'election'` otherwise
|
|
338
|
+
3. **Cloud assignment** → cloud dictates hub (has the full picture)
|
|
339
|
+
4. **Static config** → last resort
|
|
340
|
+
5. **Nothing** → `myRole = 'unassigned'`
|
|
341
|
+
|
|
342
|
+
## Hub Election
|
|
343
|
+
|
|
344
|
+
Pure deterministic election algorithm — no I/O, no side effects:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { electHub } from '@rljson/network';
|
|
348
|
+
import type { ElectionResult } from '@rljson/network';
|
|
349
|
+
|
|
350
|
+
const candidates = [
|
|
351
|
+
{ nodeId: 'node-a', host: '10.0.0.1', port: 3000, ... startedAt: 1000 },
|
|
352
|
+
{ nodeId: 'node-b', host: '10.0.0.2', port: 3000, ... startedAt: 900 },
|
|
353
|
+
];
|
|
354
|
+
const probes = [
|
|
355
|
+
{ fromNodeId: 'self', toNodeId: 'node-a', reachable: true, latencyMs: 5, measuredAt: Date.now() },
|
|
356
|
+
{ fromNodeId: 'self', toNodeId: 'node-b', reachable: true, latencyMs: 3, measuredAt: Date.now() },
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
const result: ElectionResult = electHub(candidates, probes, null, 'self');
|
|
360
|
+
// result.hubId = 'node-b' (earliest startedAt)
|
|
361
|
+
// result.reason = 'earliest-start'
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Election rules:
|
|
365
|
+
1. Filter candidates to reachable peers only (self is always reachable)
|
|
366
|
+
2. **Incumbent advantage**: keep current hub if still reachable
|
|
367
|
+
3. **Earliest `startedAt`** wins among reachable candidates
|
|
368
|
+
4. **Tiebreaker**: lexicographic `nodeId` comparison
|
|
369
|
+
|
|
370
|
+
## Probing
|
|
371
|
+
|
|
372
|
+
### PeerProber
|
|
373
|
+
|
|
374
|
+
Real TCP connect probe — tests whether a peer is reachable:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { probePeer } from '@rljson/network';
|
|
378
|
+
|
|
379
|
+
const probe = await probePeer('192.168.1.42', 3000, 'my-node', 'peer-node');
|
|
380
|
+
// { reachable: true, latencyMs: 2.45, fromNodeId: 'my-node', toNodeId: 'peer-node', ... }
|
|
381
|
+
|
|
382
|
+
// With custom timeout
|
|
383
|
+
const probe2 = await probePeer('10.0.0.1', 3000, 'my-node', 'remote', { timeoutMs: 500 });
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### ProbeScheduler
|
|
387
|
+
|
|
388
|
+
Periodically probes all known peers and detects state changes:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { ProbeScheduler } from '@rljson/network';
|
|
392
|
+
|
|
393
|
+
const scheduler = new ProbeScheduler({
|
|
394
|
+
intervalMs: 5000,
|
|
395
|
+
timeoutMs: 2000,
|
|
396
|
+
failThreshold: 3, // Consecutive failures before 'unreachable' (default: 3)
|
|
397
|
+
});
|
|
398
|
+
scheduler.start('my-node-id');
|
|
399
|
+
scheduler.setPeers([peerA, peerB]); // NodeInfo[]
|
|
400
|
+
|
|
401
|
+
scheduler.on('probes-updated', (probes) => {
|
|
402
|
+
console.log('All probe results:', probes);
|
|
403
|
+
});
|
|
404
|
+
scheduler.on('peer-unreachable', (nodeId, probe) => {
|
|
405
|
+
console.log(`${nodeId} went down!`);
|
|
406
|
+
});
|
|
407
|
+
scheduler.on('peer-reachable', (nodeId, probe) => {
|
|
408
|
+
console.log(`${nodeId} came back!`);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Manual single cycle (useful for tests)
|
|
412
|
+
const results = await scheduler.runOnce();
|
|
413
|
+
|
|
414
|
+
scheduler.stop();
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The `NetworkManager` creates and manages a `ProbeScheduler` internally.
|
|
418
|
+
Access it via `manager.getProbeScheduler()` for advanced use.
|
|
419
|
+
|
|
420
|
+
**Flap dampening**: A peer must fail `failThreshold` consecutive probes
|
|
421
|
+
(default: 3) before being declared unreachable. A single success resets
|
|
422
|
+
the counter immediately. This prevents flapping on transient network glitches.
|
|
423
|
+
|
|
424
|
+
## Example
|
|
425
|
+
|
|
426
|
+
[src/example.ts](src/example.ts)
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
# Trouble shooting
|
|
10
|
+
|
|
11
|
+
## Table of contents <!-- omit in toc -->
|
|
12
|
+
|
|
13
|
+
- [Vscode Windows: Debugging is not working](#vscode-windows-debugging-is-not-working)
|
|
14
|
+
|
|
15
|
+
## Vscode Windows: Debugging is not working
|
|
16
|
+
|
|
17
|
+
Date: 2025-03-08
|
|
18
|
+
|
|
19
|
+
⚠️ IMPORTANT: On Windows, please check out the repo on drive C. There is a bug
|
|
20
|
+
in the VS Code Vitest extension (v1.14.4), which prevents test debugging from
|
|
21
|
+
working: <https://github.com/vitest-dev/vscode/issues/548> Please check from
|
|
22
|
+
time to time if the issue has been fixed and remove this note once it is
|
|
23
|
+
resolved.
|