@persistica/flux-mesh 0.0.1 → 0.0.4

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/src/main.ts DELETED
@@ -1,680 +0,0 @@
1
- // Make TypeScript happy
2
- declare global {
3
- var meshLoadCount: number | null;
4
- }
5
-
6
- globalThis.meshLoadCount ??= 0;
7
- globalThis.meshLoadCount++;
8
-
9
- console.log(`[flux-mesh] Reloaded ${globalThis.meshLoadCount} time(s)`);
10
-
11
- // import { Elysia } from 'elysia';
12
- // import { swagger } from '@elysiajs/swagger';
13
-
14
- // if (!process.env['FLUX_MESH_JWT_KEY']) {
15
- // throw new Error('Missing FLUX_MESH_JWT_KEY in .env');
16
- // }
17
-
18
- import {
19
- type TNetworkId_S,
20
- type TAddress,
21
- type TChannelName,
22
- type TClientId,
23
- type TMachineAddress,
24
- type TProcessAddress,
25
- type TProcessId,
26
- type TAgentOwnUId,
27
- UnknownClientError,
28
- CONNECT_TO_CLIENT,
29
- SUBSCRIBE_NETWORK_CHANNEL_NAME,
30
- ERROR,
31
- RPC_RESPONSE,
32
- SUBSCRIBED_NETWORK_CHANNEL_NAME,
33
- NETWORK_CHANNEL_PUBLISH,
34
- validateChannelNameOrThrow,
35
- ON_NETWORK_CHANNEL_PUBLISH,
36
- AUTHORITY_CHANNEL_SUBSCRIBE,
37
- UNSUBSCRIBE_NETWORK_CHANNEL_NAME,
38
- UNSUBSCRIBED_NETWORK_CHANNEL_NAME,
39
- AUTHORITY_DISCONNECT_AGENT,
40
- } from '@flux/shared/types';
41
- import * as Bun from 'bun';
42
- import { nanoid } from 'nanoid';
43
- import { OutgoingMessageRouter } from './routing/outgoing-message-router.class';
44
- import { NetworkAuthorityManager } from './register/register-network-authority.class';
45
- import { verifyTokenOrThrow } from './auth/auth';
46
- import { GlobalRPCClient } from './routing/rpc/core/global-rpc-client.class';
47
- import { ProcessMessageRouter } from './routing/process-message-router.class';
48
- import {
49
- GlobalWebRTCClient,
50
- WebRTCClient,
51
- } from './_classes/web-rtc-client-interface.class';
52
- import { facilitateWebRTCConnection } from './orchestrators/facilitate-webrtc-connection.class';
53
- import {
54
- readMachineAddress,
55
- readProcessAddress,
56
- readProcessId,
57
- } from './routing/addressing.utils';
58
- import { NetworkAgentManager } from './register/network-client-manager.class';
59
- import { OPTIONS_RESPONSE } from './_routes/options.route';
60
- import { authorizeNetworkAuthority } from './_routes/auth/network-authority.post.route';
61
- import { authorizeNetworkAgent } from './_routes/auth/network-client.post.route';
62
- import type {
63
- RPCClient,
64
- RPCResponse,
65
- TRPCResponseCallbackFunction,
66
- } from '@flux/shared/ws';
67
- import * as nodeURL from 'node:url';
68
- import {
69
- type RedisConnection,
70
- getMeshRedisConnection,
71
- } from './routing/redis/redis-connection.class';
72
- import { GlobalChannelPubsub } from './routing/global-channel/global-channel-pubsub.class';
73
- import { NetworkChannelManager } from './business-logic/channels/channel-manager.class';
74
- import { isNanoId } from 'libs/flux/shared/types/src/lib/client-id.type';
75
- import { PicoLogger } from '@utils/pico-logger';
76
- import type { TFluxClientUID } from '@flux/shared/utils';
77
-
78
- PicoLogger.configure({
79
- allowScopes: '*',
80
- });
81
-
82
- export type TConnectedClientSocket = Bun.ServerWebSocket<{
83
- ip: Bun.SocketAddress | null;
84
- id: TClientId;
85
- uid?: TAgentOwnUId;
86
- address: TAddress;
87
- networkId: TNetworkId_S;
88
- isAuthority?: boolean;
89
- rtcClient?: WebRTCClient;
90
- claim?: string;
91
- rpcClient: RPCClient<'channel'>;
92
- channelNames: Set<TChannelName>;
93
- machineUID?: TFluxClientUID,
94
-
95
- // The amount of data sent
96
- throughput: {
97
- bytes: number;
98
- packets: number;
99
- };
100
- }>;
101
-
102
- const clientMap: Map<TClientId, TConnectedClientSocket> = new Map();
103
- const processId: TProcessId = readProcessId();
104
- const machineAddress: TMachineAddress = readMachineAddress();
105
- const processAddress: TProcessAddress = readProcessAddress();
106
-
107
- const clientRPCResponseCallbacks: Map<
108
- TClientId,
109
- Set<TRPCResponseCallbackFunction>
110
- > = new Map();
111
-
112
- // const incommingClientMessageRouter: IncommingClientMessageRouter = new IncommingClientMessageRouter(
113
- // (
114
- // clientId: TClientId,
115
- // onMessage: TCallbackFunction,
116
- // ) => {
117
- // clientCallbacks.set(clientId, [onMessage]);
118
- // },
119
- // );
120
-
121
- export class FluxMeshServer {
122
-
123
- private readonly redisConnection: RedisConnection = getMeshRedisConnection();
124
- private readonly onReadyListeners: Set<() => void> = new Set();
125
- private readonly bunServer: Bun.Server;
126
- private readonly globalChannelPubsub: GlobalChannelPubsub;
127
- private readonly channelManager: NetworkChannelManager;
128
-
129
- constructor(
130
- private readonly options?: {
131
- port?: number,
132
- redisConnectionString?: string;
133
- },
134
- ) {
135
- this.options = {
136
- // Defaults
137
- port: 5100,
138
- redisConnectionString: '',
139
- // Override with provided options
140
- ...this.options,
141
- };
142
-
143
- const networkAuthorityManager: NetworkAuthorityManager = new NetworkAuthorityManager();
144
- const networkAgentManager: NetworkAgentManager = new NetworkAgentManager();
145
-
146
- const outgoingMessageRouter: OutgoingMessageRouter = new OutgoingMessageRouter(
147
- // passToLocalClient:
148
- (
149
- clientId: TClientId,
150
- message: string,
151
- ) => {
152
- const client: TConnectedClientSocket | undefined =
153
- clientMap.get(clientId);
154
-
155
- if (!client) {
156
- throw new UnknownClientError(clientId, processAddress);
157
- }
158
-
159
- client.send(message);
160
- }
161
- );
162
- const processMessageRouter: ProcessMessageRouter = new ProcessMessageRouter();
163
-
164
- const globalRPCClient: GlobalRPCClient<
165
- 'authorize' | 'authorizeNetworkChannel'
166
- > = new GlobalRPCClient(outgoingMessageRouter, processMessageRouter);
167
-
168
- this.bunServer = Bun.serve({
169
- port: this.options.port,
170
- idleTimeout: 0, // deactivate timeout
171
- routes: {
172
- // ****************************************************************************
173
- // *** Authenticate Network Authority
174
- // ****************************************************************************
175
- '/auth/network-authority': {
176
- OPTIONS: OPTIONS_RESPONSE,
177
- POST: authorizeNetworkAuthority,
178
- },
179
-
180
- // ****************************************************************************
181
- // *** Authenticate Client on Network
182
- // ****************************************************************************
183
- '/auth/network-client': {
184
- OPTIONS: OPTIONS_RESPONSE,
185
- POST: (request: Bun.BunRequest) =>
186
- authorizeNetworkAgent(
187
- request,
188
- networkAuthorityManager,
189
- globalRPCClient,
190
- ),
191
- },
192
- },
193
-
194
- async fetch(
195
- request: Request,
196
- server: Bun.Server,
197
- ) {
198
- // Upgrade the request to a WebSocket
199
-
200
- // const cookies = request.headers.get('Cookie');
201
- // const token = cookies['X-Token'];
202
- // console.log('CVookies: ', cookies);
203
-
204
- const token: string | string[] | undefined = nodeURL.parse(request.url, true)
205
- .query['token'];
206
-
207
- try {
208
- const decodedToken: {
209
- networkId: TNetworkId_S;
210
- claim?: string;
211
- isAuthority?: boolean;
212
- agentUID?: string,
213
- machineUID?: TFluxClientUID,
214
- } = verifyTokenOrThrow(token) as any;
215
-
216
- const socketId: TClientId = nanoid() as TClientId;
217
-
218
- if (
219
- server.upgrade(request, {
220
- data: {
221
- ip: server.requestIP(request),
222
- id: socketId,
223
- networkId: decodedToken.networkId,
224
- isAuthority: decodedToken.isAuthority,
225
- address: `${machineAddress}/${processId}/${socketId}`,
226
- claim: decodedToken.claim,
227
- uid: decodedToken.agentUID,
228
- machineUID: decodedToken.machineUID,
229
- channelNames: new Set(),
230
- throughput: {
231
- bytes: 0,
232
- packets: 0,
233
- },
234
- },
235
- })
236
- ) {
237
- // Do not return a Response
238
- return;
239
- }
240
- } catch {
241
- console.error('Token verification failed');
242
-
243
- return new Response('Token verification failed', {
244
- status: 500,
245
- });
246
- }
247
-
248
- return new Response('Upgrade failed', { status: 500 });
249
- },
250
-
251
- websocket: {
252
- perMessageDeflate: true,
253
- maxPayloadLength: 1024 * 1024, // 1 MB
254
- // publishToSelf: true,
255
-
256
- // A socket is opened, validate it
257
- async open(
258
- _ws: TConnectedClientSocket,
259
- ): Promise<void> {
260
- clientMap.set(_ws.data.id, _ws);
261
-
262
- if (_ws.data.isAuthority) {
263
- PicoLogger.log(`👮 Authority connected at address: '${_ws.data.address}'`, 'ws-connection');
264
-
265
- await networkAuthorityManager
266
- .register(
267
- _ws.data.networkId,
268
- _ws.data.id,
269
- _ws.data.machineUID,
270
- );
271
-
272
- // Let the client detect readyState. Regular ping cannot be detected by the WebSocket client in the browser
273
- _ws.send('isReady');
274
-
275
- return;
276
- }
277
-
278
- PicoLogger.log(`🤵 Agent connected: ${_ws.data.id}`, 'ws-connection');
279
-
280
- await networkAgentManager
281
- .registerAgent(
282
- _ws.data.networkId,
283
- _ws.data.id,
284
- _ws.data.ip,
285
- _ws.data.address,
286
- _ws.data.throughput,
287
- _ws.data.uid,
288
- _ws.data.machineUID,
289
- );
290
-
291
- _ws.data.rtcClient = new WebRTCClient(
292
- processAddress,
293
- _ws.send.bind(_ws),
294
- (cb: TRPCResponseCallbackFunction) => {
295
- const cbs:
296
- | Set<TRPCResponseCallbackFunction>
297
- | undefined = clientRPCResponseCallbacks.get(
298
- _ws.data.id
299
- );
300
-
301
- clientRPCResponseCallbacks.set(
302
- _ws.data.id,
303
- cbs === undefined ? new Set([cb]) : cbs.add(cb)
304
- );
305
- }
306
- );
307
-
308
- // Let the client detect readyState. Regular ping cannot be detected by the WebSocket client in the browser
309
- _ws.send('isReady');
310
- // This will make the client retry: _ws.terminate();
311
- // ws.close(1001, 'Client not validated'); // ! Check correct error code
312
- },
313
-
314
- message: async (
315
- ws: TConnectedClientSocket,
316
- message_: string | Buffer,
317
- ) => {
318
-
319
- if (typeof message_ !== 'string') {
320
- throw new Error('Message is not a string');
321
- }
322
-
323
- // Calculate the throughput
324
- const packageSize: number = new TextEncoder().encode(message_).length;
325
- ws.data.throughput.bytes = ws.data.throughput.bytes + packageSize;
326
- ws.data.throughput.packets++;
327
-
328
- const packageType: string | undefined = message_.split(':')[0];
329
-
330
- switch (packageType) {
331
- case AUTHORITY_DISCONNECT_AGENT: {
332
- if (!ws.data.isAuthority) {
333
- ws.close(4000, 'Bad behavior');
334
- return;
335
- }
336
-
337
- const agentClientId: string = message_.substring(message_.indexOf(':') + 1);
338
-
339
- if (!isNanoId(agentClientId)) {
340
- ws.send(`${ERROR}:Invalid agent ID`);
341
- return;
342
- }
343
-
344
- // Attempt to get the client
345
- const connectedClientSocket: TConnectedClientSocket | undefined = clientMap.get(agentClientId);
346
-
347
- if (
348
- !connectedClientSocket ||
349
- (connectedClientSocket.data.networkId !== ws.data.networkId) ||
350
- (connectedClientSocket.data.isAuthority)
351
- ) {
352
- ws.send(`${ERROR}:Cannot kick agent`);
353
- return;
354
- }
355
-
356
- connectedClientSocket.close(1002, 'Kicked by authority');
357
-
358
- break;
359
- }
360
-
361
- case AUTHORITY_CHANNEL_SUBSCRIBE: {
362
- if (!ws.data.isAuthority) {
363
- ws.close(4000, 'Bad behavior');
364
- return;
365
- }
366
-
367
- // Subscribe to the events
368
- ws.subscribe(`~/networks/${ws.data.networkId}/channel-created`);
369
- ws.subscribe(`~/networks/${ws.data.networkId}/channel-empty`);
370
-
371
- break;
372
- }
373
-
374
- case NETWORK_CHANNEL_PUBLISH: {
375
- const firstColon = message_.indexOf(':');
376
- const secondColon = message_.indexOf(':', firstColon + 1);
377
-
378
- const channelName: string = message_.slice(
379
- firstColon + 1,
380
- secondColon
381
- );
382
- const data: string = message_.slice(secondColon + 1);
383
-
384
- if (validateChannelNameOrThrow(channelName)) {
385
- if (ws.data.channelNames.has(channelName)) {
386
- // Don't publish to self
387
- this.globalChannelPubsub.publish(
388
- `networks/${ws.data.networkId}/channels/${channelName}`,
389
- `${ON_NETWORK_CHANNEL_PUBLISH}:${channelName}:${data}`,
390
- ws,
391
- );
392
-
393
- // Add the usage to the channel
394
- this.channelManager
395
- .increaseUsageCount(
396
- ws.data.networkId,
397
- channelName,
398
- packageSize,
399
- );
400
- }
401
- }
402
-
403
- break;
404
- }
405
-
406
- case SUBSCRIBE_NETWORK_CHANNEL_NAME: {
407
- const channelNameString: string = message_.substring(message_.indexOf(':') + 1);
408
-
409
- try {
410
- validateChannelNameOrThrow(channelNameString);
411
- } catch {
412
- ws.send(`${ERROR}:Not authorized`);
413
- return;
414
- }
415
-
416
- const channelName: TChannelName = channelNameString as TChannelName;
417
-
418
- if (ws.data.channelNames.has(channelName)) {
419
- ws.send(`${ERROR}:Agent is already subscribed to channnel`);
420
-
421
- return;
422
- }
423
-
424
- try {
425
- const networkAuthorityAddress: TAddress = await networkAuthorityManager
426
- .resolveNetworkAuthorityAddressOrThrow(
427
- ws.data.networkId,
428
- );
429
-
430
- const canHaveMembers = await this.channelManager.canHaveMembers(
431
- ws.data.networkId,
432
- channelName,
433
- );
434
-
435
- if (!canHaveMembers) {
436
- ws.send(`${ERROR}:Channel limit is reached`);
437
- }
438
-
439
- try {
440
- const authorize: boolean = await globalRPCClient.call(
441
- networkAuthorityAddress,
442
- 'authorizeNetworkChannel',
443
- channelName,
444
- ws.data.claim
445
- );
446
-
447
- if (authorize) {
448
- ws.subscribe(`networks/${ws.data.networkId}/channels/${channelName}`);
449
- ws.send(`${SUBSCRIBED_NETWORK_CHANNEL_NAME}:${channelName}`);
450
- ws.data.channelNames.add(channelName);
451
- PicoLogger.log(`🎉 Client was authorized on channel name '${channelName}'`, 'authorized');
452
-
453
- this.channelManager
454
- .joinNetworkChannel(
455
- ws.data.networkId,
456
- channelName,
457
- ws.data.address,
458
- );
459
- } else {
460
- console.error(`Client was not authorized to connect to channel name '${channelName}'`);
461
- ws.send(`${ERROR}:Not authorized`);
462
- }
463
- } catch (error) {
464
- if (error instanceof Error) {
465
- ws.send(`${ERROR}:${error.message}`);
466
- break;
467
- }
468
-
469
- console.error('Unknown error', error);
470
- ws.send(`${ERROR}:Unknown error`);
471
- }
472
- } catch {
473
- ws.send(`${ERROR}:Not netrowk authority found`);
474
- return;
475
- }
476
- break;
477
- }
478
-
479
- case UNSUBSCRIBE_NETWORK_CHANNEL_NAME: {
480
- const channelNameString: string = message_.substring(message_.indexOf(':') + 1);
481
-
482
- try {
483
- validateChannelNameOrThrow(channelNameString);
484
- } catch {
485
- ws.send(`${ERROR}:Not authorized`);
486
- return;
487
- }
488
-
489
- const channelName: TChannelName = channelNameString as TChannelName;
490
-
491
- if (!ws.data.channelNames.has(channelName)) {
492
- ws.send(`${ERROR}:Cannot unsubscribe. Agent is not connected to the channnel`);
493
-
494
- return;
495
- }
496
-
497
- try {
498
- ws.unsubscribe(`networks/${ws.data.networkId}/channels/${channelName}`);
499
- ws.send(`${UNSUBSCRIBED_NETWORK_CHANNEL_NAME}:${channelName}`);
500
- ws.data.channelNames.delete(channelName);
501
- console.log(`🚪 Client left channel name '${channelName}'`);
502
-
503
- this.channelManager
504
- .leaveNetworkChannel(
505
- ws.data.networkId,
506
- channelName,
507
- ws.data.address,
508
- );
509
-
510
- } catch (error) {
511
- if (error instanceof Error) {
512
- ws.send(`${ERROR}:${error.message}`);
513
- break;
514
- }
515
-
516
- console.error('Unknown error', error);
517
- ws.send(`${ERROR}:Unknown error`);
518
- }
519
-
520
- break;
521
- }
522
-
523
- case RPC_RESPONSE: {
524
- const rpcResponseMessage: RPCResponse = JSON.parse(
525
- message_.substring(message_.indexOf(':') + 1)
526
- ) as RPCResponse;
527
-
528
- PicoLogger.log('📡 Received RPC response.', 'rpc');
529
-
530
- processMessageRouter.message(
531
- rpcResponseMessage.rpcProcessAddress,
532
- message_
533
- );
534
-
535
- const cbs: Set<TRPCResponseCallbackFunction> | undefined =
536
- clientRPCResponseCallbacks.get(ws.data.id);
537
-
538
- for (const clientCallback of cbs ?? []) {
539
- clientCallback(rpcResponseMessage);
540
- }
541
-
542
- break;
543
- }
544
-
545
- case CONNECT_TO_CLIENT: {
546
- const initiatingClient: WebRTCClient | undefined =
547
- ws.data.rtcClient;
548
-
549
- if (!initiatingClient) {
550
- console.warn('Initiating client could not be resolved');
551
- }
552
-
553
- const clientOwnUId: TAgentOwnUId = message_.substring(
554
- message_.indexOf(':') + 1
555
- ) as TAgentOwnUId;
556
- const networkClientAddress: TAddress =
557
- await networkAgentManager.resolveNetworkClientAddressByUid(
558
- ws.data.networkId,
559
- clientOwnUId
560
- );
561
-
562
- const remoteClient: GlobalWebRTCClient | undefined =
563
- new GlobalWebRTCClient(
564
- networkClientAddress,
565
- outgoingMessageRouter,
566
- processMessageRouter
567
- );
568
-
569
- if (!remoteClient) {
570
- console.warn('Remote client could not be resolved');
571
- }
572
-
573
- if (initiatingClient && remoteClient) {
574
- facilitateWebRTCConnection(
575
- initiatingClient,
576
- remoteClient,
577
- );
578
- } else {
579
- ws.send(`${ERROR}:RPC clients could not be resolved`);
580
-
581
- return;
582
- }
583
-
584
- break;
585
- }
586
- }
587
- },
588
-
589
- // A socket is closed
590
- close: (
591
- ws: TConnectedClientSocket,
592
- code: number,
593
- ) => {
594
- clientMap.delete(ws.data.id);
595
-
596
- if (ws.data.isAuthority) {
597
- PicoLogger.log(`🛑 Authority socket disconnecting ${code} ${ws.data.id}`, 'ws-disconnect'); // 1001
598
-
599
- networkAuthorityManager.unregister(
600
- ws.data.networkId,
601
- ws.data.address,
602
- );
603
- } else {
604
- PicoLogger.log(`🛑🤵 Agent socket disconnecting ${code} ${ws.data.id}`, 'ws-disconnect'); // 1001
605
- // Unsubscribe from topics
606
-
607
- for (const channelName of (ws.data.channelNames ?? [])) {
608
- ws.unsubscribe(
609
- `networks/${ws.data.networkId}/channels/${channelName}`
610
- );
611
- }
612
-
613
- // * Leave all channels
614
- if (ws.data.channelNames.size > 0) {
615
- this.channelManager
616
- .leaveAllNetworkChannels(
617
- ws.data.networkId,
618
- ws.data.address,
619
- ws.data.channelNames,
620
- );
621
- }
622
-
623
- networkAgentManager.unregisterNetworkAgent(
624
- ws.data.networkId,
625
- ws.data.id,
626
- ws.data.uid,
627
- );
628
- }
629
- },
630
-
631
- drain(_ws: Bun.ServerWebSocket<unknown>) {
632
- console.log('drain');
633
- }, // the socket is ready to receive more data
634
- },
635
- });
636
-
637
- this.globalChannelPubsub = new GlobalChannelPubsub(
638
- this.redisConnection,
639
- this.bunServer,
640
- processAddress,
641
- );
642
-
643
- this.channelManager = new NetworkChannelManager(this.globalChannelPubsub);
644
-
645
- // TODO: DETECT WHEN READY
646
- setTimeout(() => {
647
- console.log(`Reloaded ${(globalThis as any).meshLoadCount} time(s)`);
648
- console.log(`🚀 Flux mesh server running on localhost:${this.options?.port}`);
649
-
650
- setInterval(() => {
651
- this.redisConnection.setConnected(`${machineAddress}/${processId}`);
652
- }, 3_000);
653
-
654
- for (const cb of this.onReadyListeners) {
655
- cb();
656
- }
657
- }, 50);
658
- }
659
-
660
- public onReady(
661
- fn: () => void,
662
- ): void {
663
- this.onReadyListeners.add(fn);
664
- }
665
-
666
- /**
667
- * Gracefully shuts down the server.
668
- *
669
- * @returns { Promise<void> }
670
- */
671
- public async stop(
672
- ): Promise<void> {
673
- clientMap.clear();
674
-
675
- // 'true': Force stop and close all active connections
676
- await this.bunServer.stop(true);
677
- await this.redisConnection.setDisconnected(`${machineAddress}/${processId}`);
678
- this.redisConnection.disconnect();
679
- }
680
- }