@hive-p2p/server 1.0.19 → 1.0.20

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.
@@ -0,0 +1,254 @@
1
+ import { NetworkRenderer } from './NetworkRenderer.mjs';
2
+ import { CryptoCodex } from '../core/crypto-codex.mjs';
3
+ window.CryptoCodex = CryptoCodex; // Expose for debugging
4
+
5
+ class SimulationInterface {
6
+ #connectingWs = false;
7
+ #ws;
8
+ currentPeerId;
9
+ onSettings;
10
+ onPeersIds;
11
+ onPeerInfo;
12
+ onPeerMessage;
13
+
14
+ responseReceivedByType = {};
15
+
16
+ /** @param {function} onSettings @param {function} onPeersIds @param {function} onPeerInfo */
17
+ constructor(onSettings, onPeersIds, onPeerInfo) {
18
+ if (!onSettings || !onPeersIds || !onPeerInfo) return console.error('SimulationInterface requires three callback functions: onSettings, onPeersIds, onPeerInfo');
19
+ this.onSettings = onSettings;
20
+ this.onPeersIds = onPeersIds;
21
+ this.onPeerInfo = onPeerInfo;
22
+ this.#setupWs();
23
+ window.addEventListener('beforeunload', () => this.#ws ? this.#ws.close() : null);
24
+ setInterval(() => { if (this.currentPeerId) this.getPeerInfo(this.currentPeerId) }, 300);
25
+ setInterval(() => { this.getPeerIds() }, 5000);
26
+ }
27
+
28
+ #setupWs(url = 'ws://localhost:3000') {
29
+ if (this.#ws) this.#ws.close();
30
+ this.#connectingWs = true;
31
+ this.#ws = new WebSocket(url);
32
+ this.#ws.onmessage = (event) => {
33
+ const msg = JSON.parse(event.data);
34
+
35
+ if (msg.type === 'settings') this.onSettings(msg.data);
36
+ if (msg.type === 'peersIds') this.onPeersIds(msg.data);
37
+ if (msg.type === 'peerInfo') this.onPeerInfo(msg.data);
38
+ if (msg.type === 'peerMessage' && this.onPeerMessage) this.onPeerMessage(msg.remoteId, msg.data);
39
+ };
40
+ this.#connectingWs = false;
41
+ }
42
+
43
+ start(settings) { this.#sendWsMessage({ type: 'start', settings }); }
44
+ getPeerInfo() {
45
+ this.#sendWsMessage({ type: 'getPeerInfo', peerId: this.currentPeerId });
46
+ }
47
+ getPeerIds() {
48
+ this.#sendWsMessage({ type: 'getPeersIds' });
49
+ }
50
+ tryToConnectNode(fromId, targetId) {
51
+ if (!fromId || !targetId) return;
52
+ this.#sendWsMessage({ type: 'tryToConnectNode', fromId, targetId });
53
+ }
54
+ #sendWsMessage(msg, avoidSendingIfNotAnswered = false) {
55
+ if (this.#ws?.readyState === WebSocket.OPEN) {
56
+ if (avoidSendingIfNotAnswered && this.responseReceivedByType[msg.type] === false) return;
57
+ this.responseReceivedByType[msg.type] = false;
58
+ this.#ws.send(JSON.stringify(msg));
59
+ } else {
60
+ console.error(`WebSocket is not connected. ${this.#connectingWs ? 'Trying to connect...' : ''}`);
61
+ setTimeout(() => { if (!this.#connectingWs) this.#setupWs(); }, 2000);
62
+ }
63
+ }
64
+ }
65
+
66
+ class NetworkVisualizer {
67
+ autoSelectCurrentPeerCategory = ['standard', 'public']; // 'public' | 'standard' | false
68
+ currentPeerId;
69
+ lastPeerInfo;
70
+ networkRenderer = new NetworkRenderer();
71
+ simulationInterface;
72
+
73
+ peersList = {};
74
+ elements = {
75
+ peersList: document.getElementById('peersList'),
76
+ simulationSettings: document.getElementById('simulationSettings'),
77
+
78
+ publicPeersCount: document.getElementById('publicPeersCount'),
79
+ peersCount: document.getElementById('peersCount'),
80
+ startSimulation: document.getElementById('startSimulation'),
81
+ }
82
+
83
+ constructor(isSimulation = true) {
84
+ if (isSimulation) {
85
+ this.elements.peersList.style.display = 'block';
86
+ this.elements.simulationSettings.style.display = 'block';
87
+ this.simulationInterface = new SimulationInterface(
88
+ (settings) => this.#handleSettings(settings), // event: onSettings
89
+ (peersIds) => this.#updatePeersList(peersIds), // event: onPeersIds
90
+ (data) => { if (data.peerId === this.currentPeerId) this.#updateNetworkFromPeerInfo(data.peerInfo); } // event: onPeerInfo
91
+ );
92
+
93
+ this.simulationInterface.onPeerMessage = (remoteId, data) => {
94
+ if (data.route) this.networkRenderer.displayDirectMessageRoute(remoteId, data.route);
95
+ else if (data.topic) this.networkRenderer.displayGossipMessageRoute(remoteId, data.senderId, data.topic, data.data);
96
+ };
97
+
98
+ this.networkRenderer.onNodeLeftClick = (nodeId) => this.simulationInterface.tryToConnectNode(this.currentPeerId,nodeId);
99
+ this.networkRenderer.onNodeRightClick = (nodeId) => this.#setSelectedPeer(nodeId);
100
+ this.elements.startSimulation.onclick = () => {
101
+ this.mockRunning = false;
102
+ this.networkRenderer.clearNetwork();
103
+ this.simulationInterface.start(this.#getSimulatorSettings());
104
+ }
105
+ }
106
+
107
+ setInterval(() => this.networkRenderer.updateStats(), 200);
108
+
109
+ window.addEventListener('keydown', (e) => {
110
+ if (e.key === 'ArrowUp') this.#selectPreviousPeer();
111
+ if (e.key === 'ArrowDown') this.#selectNextPeer();
112
+ });
113
+ }
114
+
115
+ #setSelectedPeer(peerId) {
116
+ if (!peerId) return;
117
+ if (this.networkRenderer.currentPeerId !== peerId) {
118
+ console.log(`Selected peer changed, now => ${peerId}`);
119
+ this.networkRenderer.maxDistance = 0; // reset maxDistance to show all nodes
120
+ this.networkRenderer.avoidAutoZoomUntil = Date.now() + 2000; // avoid auto-zoom for 2 seconds
121
+ this.networkRenderer.lastAutoZoomDistance = 0;
122
+ this.networkRenderer.clearNetwork(); // Clear network and scroll peerId into view
123
+
124
+ for (const peerItem of document.querySelectorAll(`#peersList div[data-peer-id]`))
125
+ if (peerItem.dataset.peerId === peerId) peerItem.classList.add('selected');
126
+ else peerItem.classList.remove('selected');
127
+
128
+ const selectedItem = document.querySelector(`#peersList div[data-peer-id="${peerId}"]`);
129
+ if (selectedItem) {
130
+ const listRect = this.elements.peersList.getBoundingClientRect();
131
+ const itemRect = selectedItem.getBoundingClientRect();
132
+ if (itemRect.top < listRect.top || itemRect.bottom > listRect.bottom) {
133
+ this.elements.peersList.scrollTop = selectedItem.offsetTop - this.elements.peersList.offsetTop - this.elements.peersList.clientHeight / 2 + selectedItem.clientHeight / 2;
134
+ }
135
+ }
136
+ }
137
+ this.networkRenderer.currentPeerId = peerId;
138
+
139
+ this.#setCurrentPeer(peerId);
140
+ this.simulationInterface.getPeerInfo();
141
+
142
+ }
143
+ #selectNextPeer() {
144
+ const peerIds = Object.keys(this.peersList);
145
+ if (peerIds.length === 0) return;
146
+ if (!this.currentPeerId) return this.#setSelectedPeer(peerIds[0]);
147
+ const currentIndex = peerIds.indexOf(this.currentPeerId);
148
+ const nextIndex = (currentIndex + 1) % peerIds.length;
149
+ this.#setSelectedPeer(peerIds[nextIndex]);
150
+ }
151
+ #selectPreviousPeer() {
152
+ const peerIds = Object.keys(this.peersList);
153
+ if (peerIds.length === 0) return;
154
+ if (!this.currentPeerId) return this.#setSelectedPeer(peerIds[0]);
155
+ const currentIndex = peerIds.indexOf(this.currentPeerId);
156
+ const previousIndex = (currentIndex - 1 + peerIds.length) % peerIds.length;
157
+ this.#setSelectedPeer(peerIds[previousIndex]);
158
+ }
159
+ #setCurrentPeer(id, clearNetworkOneChange = true) {
160
+ this.currentPeerId = id;
161
+ this.simulationInterface.currentPeerId = id;
162
+ this.networkRenderer.setCurrentPeer(id, clearNetworkOneChange);
163
+ }
164
+ #getSimulatorSettings() {
165
+ return {
166
+ publicPeersCount: parseInt(this.elements.publicPeersCount.value),
167
+ peersCount: parseInt(this.elements.peersCount.value)
168
+ };
169
+ }
170
+ // LIVE METHODS
171
+ #updateNetworkFromPeerInfo(peerInfo) {
172
+ if (!peerInfo) return;
173
+ this.lastPeerInfo = peerInfo;
174
+
175
+ const newlyUpdated = {};
176
+ const digestPeerUpdate = (id = 'toto', status = 'unknown', neighbors = []) => {
177
+ const isPublic = CryptoCodex.isPublicNode(id);
178
+ this.networkRenderer.addOrUpdateNode(id, status, isPublic, neighbors);
179
+ newlyUpdated[id] = true;
180
+ }
181
+
182
+ const getNeighbors = (peerId) => {
183
+ const knownPeer = peerInfo.store.known[peerId];
184
+ return knownPeer ? Object.keys(knownPeer.neighbors || {}) : [];
185
+ }
186
+
187
+ const knownToIgnore = {};
188
+ knownToIgnore[this.currentPeerId] = true;
189
+ for (const id of peerInfo.store.connecting) knownToIgnore[id] = true;
190
+ for (const id of peerInfo.store.connected) knownToIgnore[id] = true;
191
+ for (const id in peerInfo.store.known)
192
+ if (!knownToIgnore[id]) digestPeerUpdate(id, 'known', getNeighbors(id));
193
+
194
+ for (const id of peerInfo.store.connecting) digestPeerUpdate(id, 'connecting', getNeighbors(id));
195
+ for (const id of peerInfo.store.connected) digestPeerUpdate(id, 'connected', getNeighbors(id));
196
+
197
+
198
+ const nodes = this.networkRenderer.nodesStore.store;
199
+ const nodeIds = this.networkRenderer.nodesStore.getNodesIds();
200
+ for (const id of nodeIds) // filter absent nodes
201
+ if (!newlyUpdated[id] && id !== this.currentPeerId) this.networkRenderer.removeNode(id);
202
+
203
+ // ensure current peer is updated
204
+ if (peerInfo.id === this.currentPeerId) digestPeerUpdate(peerInfo.id, 'current', getNeighbors(peerInfo.id));
205
+
206
+ // Create connections
207
+ const connections = [];
208
+ for (const id in nodes)
209
+ for (const neighborId of nodes[id].neighbors) connections.push([id, neighborId]);
210
+
211
+ //console.log(`Updated network map: ${Object.keys(nodes).length} nodes | ${Object.keys(connections).length} connections`);
212
+ this.networkRenderer.digestConnectionsArray(connections);
213
+ }
214
+ // SIMULATION METHODS
215
+ #handleSettings(settings = { publicPeersCount: 2, peersCount: 5 }) {
216
+ if (settings.publicPeersCount) this.elements.publicPeersCount.value = settings.publicPeersCount;
217
+ if (settings.peersCount) this.elements.peersCount.value = settings.peersCount;
218
+ if (settings.autoStart) this.simulationInterface.getPeerIds();
219
+ }
220
+ #createPeerItem(peerId) {
221
+ const peerItem = document.createElement('div');
222
+ peerItem.dataset.peerId = peerId;
223
+ peerItem.textContent = peerId;
224
+ peerItem.onclick = () => this.#setSelectedPeer(peerId);
225
+ return peerItem;
226
+ }
227
+ #updatePeersList(peersData, element = this.elements.peersList) {
228
+ //element.innerHTML = '<h3>Peers list</h3>';
229
+
230
+ const peerIds = {};
231
+ for (const category in peersData)
232
+ for (const peerId of peersData[category]) {
233
+ peerIds[peerId] = true;
234
+ if (this.peersList[peerId]) continue; // already listed
235
+ const peerItem = this.#createPeerItem(peerId);
236
+ element.appendChild(peerItem);
237
+ this.peersList[peerId] = peerItem;
238
+ }
239
+
240
+ for (const peerId of Object.keys(this.peersList)) // remove absent peers
241
+ if (!peerIds[peerId]) { this.peersList[peerId].remove(); delete this.peersList[peerId]; }
242
+
243
+ if (this.currentPeerId) return this.#setSelectedPeer(this.currentPeerId); // Auto-select current peer
244
+
245
+ for (const category of this.autoSelectCurrentPeerCategory)
246
+ for (const peerId of peersData[category] || []) return this.#setSelectedPeer(peerId);
247
+ }
248
+ }
249
+
250
+ const networkVisualizer = new NetworkVisualizer(true);
251
+ if (typeof window !== 'undefined') {
252
+ window.networkVisualizer = networkVisualizer; // Expose for debugging
253
+ window.networkRenderer = networkVisualizer.networkRenderer; // Expose for debugging
254
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Synchronized Network Clock
3
+ * Simple, efficient NTP-based time synchronization
4
+ */
5
+
6
+ export class Clock {
7
+ verbose;
8
+ mockMode; // if true, use local time without sync
9
+ static #instance = null;
10
+
11
+ #offset = null; // ms difference from local time
12
+ #syncing = false;
13
+ #lastSync = 0;
14
+ #sources = ['time.google.com', 'time.cloudflare.com', 'pool.ntp.org'];
15
+ #browserSources = [
16
+ 'worldtimeapi.org/api/timezone/UTC',
17
+ 'timeapi.io/api/Time/current/zone?timeZone=UTC',
18
+ 'api.github.com'
19
+ ];
20
+
21
+ constructor(verbose = 0, mockMode = false) {
22
+ this.verbose = verbose;
23
+ this.mockMode = mockMode;
24
+ if (Clock.#instance) return Clock.#instance;
25
+ else Clock.#instance = this;
26
+ }
27
+
28
+ // PUBLIC API
29
+ static get instance() { return Clock.#instance || new Clock(); }
30
+ static get time() { return Clock.instance.time; }
31
+ get time() {
32
+ if (this.mockMode) return Date.now();
33
+ if (this.#offset === null) return null;
34
+ return Date.now() + Math.round(this.#offset);
35
+ }
36
+
37
+ async sync(verbose) {
38
+ if (verbose !== undefined) this.verbose = verbose;
39
+ if (this.mockMode) return Date.now();
40
+
41
+ if (this.#syncing) {
42
+ while (this.#syncing) await new Promise(resolve => setTimeout(resolve, 50));
43
+ return this.time;
44
+ }
45
+
46
+ this.#syncing = true;
47
+
48
+ try {
49
+ const samples = await this.#fetchTimeSamples();
50
+ if (samples.length === 0) {
51
+ console.warn('[Clock] All NTP sources failed, using local time');
52
+ this.#offset = 0;
53
+ return this.time;
54
+ }
55
+
56
+ this.#offset = this.#calculateOffset(samples);
57
+ this.#lastSync = Date.now();
58
+
59
+ if (samples.length < this.#sources.length)
60
+ setTimeout(() => this.#backgroundRefine(), 100);
61
+ return this.time;
62
+ } catch (error) {
63
+ console.error('[Clock] Sync failed:', error);
64
+ this.#offset = 0;
65
+ return this.time;
66
+ } finally {
67
+ this.#syncing = false;
68
+ }
69
+ }
70
+
71
+ get status() {
72
+ if (this.mockMode) return {
73
+ synchronized: true, syncing: false, offset: 0,
74
+ lastSync: Date.now(), age: 0
75
+ };
76
+ return {
77
+ synchronized: this.#offset !== null,
78
+ syncing: this.#syncing,
79
+ offset: this.#offset,
80
+ lastSync: this.#lastSync,
81
+ age: this.#lastSync ? Date.now() - this.#lastSync : null
82
+ };
83
+ }
84
+
85
+ // PRIVATE METHODS
86
+ async #fetchTimeSamples() {
87
+ const sources = (typeof window !== 'undefined') ? this.#browserSources : this.#sources;
88
+ const promises = sources.map(source => this.#fetchTimeFromSource(source));
89
+ const results = await Promise.allSettled(promises);
90
+
91
+ const samples = [];
92
+ for (const result of results) {
93
+ if (result.status === 'fulfilled' && result.value) {
94
+ samples.push(result.value);
95
+ }
96
+ }
97
+ return samples;
98
+ }
99
+
100
+ async #fetchTimeFromSource(source) {
101
+ const controller = new AbortController();
102
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // Plus de timeout
103
+
104
+ try {
105
+ const startTime = Date.now();
106
+ const url = source.startsWith('http') ? source : `https://${source}`;
107
+
108
+ const response = await fetch(url, {
109
+ method: source.includes('api.github.com') ? 'HEAD' : 'GET',
110
+ signal: controller.signal,
111
+ cache: 'no-cache',
112
+ mode: 'cors' // Explicit CORS
113
+ });
114
+
115
+ const networkLatency = (Date.now() - startTime) / 2;
116
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
117
+
118
+ let serverTime;
119
+
120
+ if (source.includes('worldtimeapi')) {
121
+ const data = await response.json();
122
+ serverTime = new Date(data.utc_datetime).getTime();
123
+ } else if (source.includes('timeapi')) {
124
+ const data = await response.json();
125
+ serverTime = new Date(data.dateTime).getTime();
126
+ } else {
127
+ const dateHeader = response.headers.get('date');
128
+ if (!dateHeader) throw new Error('No date header');
129
+ serverTime = new Date(dateHeader).getTime();
130
+ }
131
+
132
+ if (isNaN(serverTime)) throw new Error('Invalid time data');
133
+
134
+ return {
135
+ source,
136
+ serverTime: serverTime + networkLatency,
137
+ localTime: Date.now(),
138
+ latency: networkLatency * 2
139
+ };
140
+ } catch (error) {
141
+ if (this.verbose) console.warn(`[Clock] Failed to fetch time from ${source}:`, error.message);
142
+ return null; // Retourne explicitement null au lieu d'undefined
143
+ } finally {
144
+ clearTimeout(timeoutId);
145
+ }
146
+ }
147
+
148
+ #calculateOffset(samples) {
149
+ // Filter out null samples (au cas où)
150
+ const validSamples = samples.filter(sample => sample !== null);
151
+ if (validSamples.length === 0) return 0;
152
+ if (validSamples.length === 1) return validSamples[0].serverTime - validSamples[0].localTime;
153
+
154
+ validSamples.sort((a, b) => a.latency - b.latency);
155
+
156
+ const offsets = [];
157
+ for (const sample of validSamples)
158
+ offsets.push(sample.serverTime - sample.localTime);
159
+
160
+ offsets.sort((a, b) => a - b);
161
+ const mid = Math.floor(offsets.length / 2);
162
+
163
+ if (offsets.length % 2 === 0)
164
+ return (offsets[mid - 1] + offsets[mid]) / 2;
165
+ return offsets[mid];
166
+ }
167
+
168
+ async #backgroundRefine() {
169
+ if (this.#syncing) return;
170
+
171
+ try {
172
+ const samples = await this.#fetchTimeSamples();
173
+ if (samples.length === 0) return;
174
+
175
+ const newOffset = this.#calculateOffset(samples);
176
+ if (Math.abs(newOffset - this.#offset) > 100)
177
+ this.#offset = newOffset;
178
+ } catch (error) {
179
+ if (this.verbose) console.warn('[Clock] Background refine failed:', error);
180
+ }
181
+ }
182
+ }
183
+
184
+ export async function CLOCK_TEST() {
185
+ const startTime = Date.now();
186
+ const clock = new Clock(1); // Verbose pour debug
187
+
188
+ try {
189
+ await clock.sync();
190
+ console.log('Synchronized in:', Date.now() - startTime, 'ms');
191
+ console.log('Synchronized time:', new Date(clock.time).toISOString());
192
+ console.log('Clock status:', clock.status);
193
+ } catch (error) {
194
+ console.error('Sync failed:', error);
195
+ }
196
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Synchronized Network Clock
3
+ * Simple, efficient NTP-based time synchronization
4
+ */
5
+
6
+ export class Clock {
7
+ verbose;
8
+ mockMode; // if true, use local time without sync
9
+ static #instance = null;
10
+
11
+ #offset = null; // ms difference from local time
12
+ #syncing = false;
13
+ #lastSync = 0;
14
+ #sources = ['time.google.com', 'time.cloudflare.com', 'pool.ntp.org'];
15
+
16
+ constructor(verbose = 0, mockMode = false) {
17
+ this.verbose = verbose;
18
+ this.mockMode = mockMode;
19
+ if (Clock.#instance) return Clock.#instance;
20
+ else Clock.#instance = this;
21
+ }
22
+
23
+ // PUBLIC API
24
+ static get instance() { return Clock.#instance || new Clock(); }
25
+ static get time() { return Clock.instance.time; } // Sync API - returns current synchronized time or null
26
+ get time() {
27
+ if (this.mockMode) return Date.now();
28
+ if (this.#offset === null) return null;
29
+ return Date.now() + Math.round(this.#offset);
30
+ }
31
+ async sync(verbose) { // Force synchronization - returns promise with synchronized time
32
+ if (verbose !== undefined) this.verbose = verbose;
33
+ if (this.mockMode) return Date.now(); // Bypass sync in mock mode
34
+
35
+ if (this.#syncing) { // Wait for current sync to complete
36
+ while (this.#syncing) await new Promise(resolve => setTimeout(resolve, 50));
37
+ return this.time;
38
+ }
39
+
40
+ this.#syncing = true;
41
+
42
+ try {
43
+ const samples = await this.#fetchTimeSamples();
44
+ if (samples.length === 0) {
45
+ console.warn('[Clock] All NTP sources failed, using local time');
46
+ this.#offset = 0;
47
+ return this.time;
48
+ }
49
+
50
+ this.#offset = this.#calculateOffset(samples);
51
+ this.#lastSync = Date.now();
52
+
53
+ // Continue refining in background if we got partial results
54
+ if (samples.length < this.#sources.length) setTimeout(() => this.#backgroundRefine(), 100);
55
+ return this.time;
56
+ } catch (error) {
57
+ console.error('[Clock] Sync failed:', error);
58
+ this.#offset = 0; // Fallback to local time
59
+ return this.time;
60
+ } finally { this.#syncing = false; }
61
+ }
62
+ get status() { // Get sync status info
63
+ if (this.mockMode) return { synchronized: true, syncing: false, offset: 0, lastSync: Date.now(), age: 0 };
64
+ return {
65
+ synchronized: this.#offset !== null,
66
+ syncing: this.#syncing,
67
+ offset: this.#offset,
68
+ lastSync: this.#lastSync,
69
+ age: this.#lastSync ? Date.now() - this.#lastSync : null
70
+ };
71
+ }
72
+
73
+ // PRIVATE METHODS
74
+ async #fetchTimeSamples() { // Fetch time samples from all sources in parallel
75
+ const promises = this.#sources.map(source => this.#fetchTimeFromSource(source));
76
+ const results = await Promise.allSettled(promises);
77
+ const samples = [];
78
+ for (const result of results) if (result.status === 'fulfilled') samples.push(result.value);
79
+ return samples;
80
+ }
81
+ /** @param {'time.google.com' | 'time.cloudflare.com' | 'pool.ntp.org'} source */
82
+ async #fetchTimeFromSource(source) { // Fetch time from a single NTP source
83
+ const controller = new AbortController();
84
+ const timeoutId = setTimeout(() => controller.abort(), 2000); // 2s timeout
85
+
86
+ try {
87
+ const startTime = Date.now();
88
+ const response = await fetch(`https://${source}`, { method: 'HEAD', signal: controller.signal, cache: 'no-cache' });
89
+ const networkLatency = (Date.now() - startTime) / 2; // Rough RTT/2
90
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
91
+
92
+ const serverTime = new Date(response.headers.get('date')).getTime();
93
+ if (isNaN(serverTime)) throw new Error('Invalid date header');
94
+
95
+ return {
96
+ source,
97
+ serverTime: serverTime + networkLatency, // Compensate for network delay
98
+ localTime: Date.now(),
99
+ latency: networkLatency * 2
100
+ };
101
+ } finally { clearTimeout(timeoutId); }
102
+ }
103
+ /** @param {Array<{serverTime: number, localTime: number, latency: number}>} samples */
104
+ #calculateOffset(samples) { // Calculate offset from multiple samples
105
+ if (samples.length === 1) return samples[0].serverTime - samples[0].localTime;
106
+ samples.sort((a, b) => a.latency - b.latency); // Sort by latency, prefer lower latency sources
107
+
108
+ const offsets = [];
109
+ for (const sample of samples) offsets.push(sample.serverTime - sample.localTime);
110
+ offsets.sort((a, b) => a - b); // Use median to filter outliers
111
+ const mid = Math.floor(offsets.length / 2);
112
+
113
+ if (offsets.length % 2 === 0) return (offsets[mid - 1] + offsets[mid]) / 2;
114
+ return offsets[mid];
115
+ }
116
+ async #backgroundRefine() { // Background refinement after initial sync
117
+ if (this.#syncing) return;
118
+
119
+ try {
120
+ const samples = await this.#fetchTimeSamples();
121
+ if (samples.length === 0) return; // All failed
122
+
123
+ // Only update if change is significant (> 100ms)
124
+ const newOffset = this.#calculateOffset(samples);
125
+ if (Math.abs(newOffset - this.#offset) > 100) this.#offset = newOffset;
126
+ } catch (error) { if (this.verbose) console.warn('[Clock] Background refine failed:', error); }
127
+ }
128
+ }
129
+
130
+
131
+ export async function CLOCK_TEST() { // DEBUG TEST WHILE RUNNING AS STANDALONE
132
+ const startTime = Date.now();
133
+ const clock = new Clock();
134
+ clock.sync().then(() => {
135
+ console.log('Synchronized in: ', Date.now() - startTime, 'ms');
136
+ console.log('Synchronized time:', new Date(clock.time).toISOString());
137
+ console.log('Clock status:', clock.status);
138
+ }).catch(console.error);
139
+
140
+ while (true) {
141
+ console.log('Clock status:', clock.status);
142
+ await new Promise(r => setTimeout(r, 1000));
143
+ }
144
+ }
@@ -0,0 +1,64 @@
1
+ export class Converter {
2
+ IS_BROWSER = typeof window !== 'undefined';
3
+ FROMBASE64_AVAILABLE = typeof Uint8Array.fromBase64 === 'function';
4
+ textEncoder = new TextEncoder();
5
+ textDecoder = new TextDecoder();
6
+ buffer2 = new ArrayBuffer(2); view2 = new DataView(this.buffer2);
7
+ buffer4 = new ArrayBuffer(4); view4 = new DataView(this.buffer4);
8
+ buffer8 = new ArrayBuffer(8); view8 = new DataView(this.buffer8);
9
+ hexMap = { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15 };
10
+
11
+ // ... TO BYTES
12
+ /** Number should be between 0 and 65535 @param {number} num - Integer to convert to 2 bytes Uint8Array */
13
+ numberTo2Bytes(num) { this.view2.setUint16(0, num, true); return new Uint8Array(this.buffer2); }
14
+ /** Number should be between 0 and 4294967295 @param {number} num - Integer to convert to 4 bytes Uint8Array */
15
+ numberTo4Bytes(num) { this.view4.setUint32(0, num, true); return new Uint8Array(this.buffer4); }
16
+ /** Number should be between 0 and 18446744073709551615 @param {number} num - Integer to convert to 8 bytes Uint8Array */
17
+ numberTo8Bytes(num) { this.view8.setBigUint64(0, BigInt(num), true); return new Uint8Array(this.buffer8); }
18
+ stringToBytes(str = 'toto') { return this.textEncoder.encode(str); }
19
+ /** @param {string} hex - Hex string to convert to Uint8Array */
20
+ hexToBytes(hex) {
21
+ const length = hex.length / 2;
22
+ const uint8Array = new Uint8Array(length);
23
+ for (let i = 0, j = 0; i < length; ++i, j += 2) uint8Array[i] = (this.hexMap[hex[j]] << 4) + this.hexMap[hex[j + 1]];
24
+ return uint8Array;
25
+ }
26
+ /** Base64 string to convert to Uint8Array @param {string} base64 @returns {Uint8Array} */
27
+ base64toBytes(base64) {
28
+ if (!this.IS_BROWSER) return new Uint8Array(Buffer.from(base64, 'base64'));
29
+ if (this.FROMBASE64_AVAILABLE) return Uint8Array.fromBase64(base64);
30
+ const binaryString = atob(base64);
31
+ const bytes = new Uint8Array(binaryString.length);
32
+ for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
33
+ return bytes;
34
+ }
35
+ // BYTES TO ...
36
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
37
+ bytes2ToNumber(uint8Array) { for (let i = 0; i < 2; i++) this.view2.setUint8(i, uint8Array[i]); return this.view2.getUint16(0, true); }
38
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
39
+ bytes4ToNumber(uint8Array) { for (let i = 0; i < 4; i++) this.view4.setUint8(i, uint8Array[i]); return this.view4.getUint32(0, true); }
40
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
41
+ bytes8ToNumber(uint8Array) { for (let i = 0; i < 8; i++) this.view8.setUint8(i, uint8Array[i]); return Number(this.view8.getBigUint64(0, true)); }
42
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
43
+ bytesToString(uint8Array) { return this.textDecoder.decode(uint8Array); }
44
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
45
+ static bytesToHex(uint8Array, minLength = 0) {
46
+ let hexStr = '';
47
+ for (const byte of uint8Array) hexStr += byte < 16 ? `0${byte.toString(16)}` : byte.toString(16);
48
+ if (minLength > 0) { hexStr = hexStr.padStart(minLength, '0'); }
49
+ return hexStr;
50
+ }
51
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
52
+ bytesToHex(uint8Array, minLength = 0) { return Converter.bytesToHex(uint8Array, minLength); }
53
+ // OTHERS
54
+ /** @param {string} hex - Hex string to convert to bits @param {'string' | 'arrayOfString' | 'arrayOfNumbers'} format - Output format, default: string */
55
+ static hexToBits(hex = 'ffffff', format = 'string') {
56
+ let bitsString = []; // WE USE STRING METHODS FOR PERFORMANCE (number '0' = 8 Bytes | string '0' = 1 Byte)
57
+ if (format === 'arrayOfNumbers') for (let i = 0; i < hex.length; i++) bitsString.push(...(parseInt(hex[i], 16).toString(2).padStart(4, '0').split('').map(b => parseInt(b, 10))));
58
+ else for (let i = 0; i < hex.length; i++) bitsString.push(parseInt(hex[i], 16).toString(2).padStart(4, '0'));
59
+ return format === 'string' ? bitsString.join('') : bitsString;
60
+ }
61
+ /** @param {Uint8Array} uint8Array - Uint8Array to convert to bits @param {'string' | 'arrayOfString' | 'arrayOfNumbers'} format - Output format, default: string */
62
+ static bytesToBits(uint8Array, format = 'string') { return this.hexToBits(this.bytesToHex(uint8Array), format); }
63
+ static ipToInt(ip = '192.168.0.1') { return ip.split('.').reduce((int, oct) => (int << 8) + parseInt(oct, 10), 0); }
64
+ }