@yz-social/civildefense.io 4.5.9 → 4.5.21

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/server/app.js CHANGED
@@ -1,11 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
- import { exec } from 'node:child_process';
4
3
  import {cpus, availableParallelism } from 'node:os';
5
4
  import cluster from 'node:cluster';
6
- import http from 'node:http';
7
- import express from 'express';
8
- import logger from 'morgan';
9
5
  import yargs from 'yargs';
10
6
  import { hideBin } from 'yargs/helpers';
11
7
  import { resolve } from './dirname.js';
@@ -18,12 +14,12 @@ const argv = yargs(hideBin(process.argv))
18
14
  .option('nPortals', {
19
15
  alias: 'p',
20
16
  type: 'number',
21
- default: Math.min(logicalCores, 5),
17
+ default: Math.max(2, logicalCores - 3),
22
18
  description: "The number of steady nodes that handle initial connections."
23
19
  })
24
20
  .option('baseURL', {
25
21
  type: 'string',
26
- default: 'http://localhost:3000/kdht',
22
+ default: 'http://localhost:3000',
27
23
  description: "The base URL of the portal server through which to bootstrap."
28
24
  })
29
25
  .option('externalBaseURL', {
@@ -36,15 +32,10 @@ const argv = yargs(hideBin(process.argv))
36
32
  default: false,
37
33
  description: "Announce our availability on the DHT, so that other nodes can enter through us if their original portal goes down."
38
34
  })
39
- .option('fixedSpacing', {
35
+ .option('spacing', {
40
36
  type: 'number',
41
- default: 2,
42
- description: "Minimum seconds to add between each portal."
43
- })
44
- .options('variableSpacing', {
45
- type: 'number',
46
- default: 5,
47
- description: "Additional variable seconds (+/- variableSpacing/2) to add to fixedSpacing between each portal."
37
+ default: 3,
38
+ description: "Minimum seconds to add between each portal on startup and shutdown."
48
39
  })
49
40
  .option('info', {
50
41
  alias: 'i',
@@ -60,11 +51,22 @@ const argv = yargs(hideBin(process.argv))
60
51
  })
61
52
  .parse();
62
53
 
54
+ const log = argv.info && ((...rest) => console.log(new Date(), ...rest));
55
+ const debug = argv.verbose && ((...rest) => console.log(new Date(), ...rest));
56
+ function delay(ms = argv.spacing * 1e3) { return new Promise(resolve => setTimeout(resolve, ms)); }
57
+
63
58
  if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
59
+ const http = await import('node:http');
60
+ const express = (await import('express')).default;
61
+ const logger = (await import('morgan')).default;
62
+ const { configureWebsocket } = await import('./websocket.js');
63
+
64
64
  const port = parseInt((new URL(argv.baseURL)).port || '80');
65
65
  process.title = 'yz.social';
66
66
  const app = express();
67
67
  app.use(logger(':date[iso] :status :method :url :res[content-length] - :response-time ms'));
68
+ const server = http.createServer(app);
69
+ configureWebsocket(server);
68
70
 
69
71
  // if (argv.announce) { // The default is to not announce.
70
72
  // let announce = null;
@@ -99,32 +101,44 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
99
101
  extensions: ['js'] // Some dependencies refer to .js files as relative pathnames, with the .js missing.
100
102
  }));
101
103
 
102
- app.listen(port);
103
- console.log(new Date(), `Listening on ${port} and starting ${argv.nPortals} nodes on ${logicalCores} ${cpus()[0].model} logical cores.`);
104
+ server.listen(port);
105
+ log(`Listening on ${port} and starting ${argv.nPortals} nodes on ${logicalCores} ${cpus()[0].model} logical cores.`);
104
106
  for (let i = 0; i < argv.nPortals; i++) {
105
107
  cluster.fork();
106
- await new Promise(resolve => setTimeout(resolve, 2e3)); // Number chosen to give an unspikey rise in packets/s.
108
+ await delay(); // Number chosen to give an unspikey rise in packets/s.
107
109
  }
110
+ log(process.title, 'RUNNING.');
111
+ process.on('SIGINT', async () => { // Leave the network politely.
112
+ const seconds = (1 + argv.nPortals) * argv.spacing;
113
+ log(process.title, 'shutting down', argv.nPortals, 'nodes.');
114
+ await delay(seconds * 1e3);
115
+ log(process.title, 'done');
116
+ process.exit(0);
117
+ });
108
118
  } else {
109
119
  process.title = 'axona-starting';
110
120
  const { P2PWebNetwork, location } = await import('../index.js');
111
121
  const network = await P2PWebNetwork.create({
112
122
  region: location,
113
- infoLogger: (...rest) => argv.info && console.log(new Date(), ...rest),
114
- debugLogger: (...rest) => argv.verbose && console.log(new Date(), ...rest)
123
+ infoLogger: log,
124
+ debugLogger: debug
115
125
  });
116
126
  process.title = 'axona-' + network.nodeIdentity.id;
117
- let update = setInterval(() => {
118
- const roles = network.peer.health().axonRoles;
119
- network.debug(roles.length, 'axons',
120
- roles.reduce((total, role) => total + (role.isRoot ? 1 : 0), 0), 'roots');
121
- }, 10e3);
127
+ let update = argv.verbose && setInterval(() => {
128
+ network.debug(network.peer.health());
129
+ }, 30e3);
122
130
  process.on('SIGINT', async () => { // Leave the network politely.
123
- console.log(process.title, 'Shutdown for Ctrl+C');
124
- clearInterval(update)
131
+ const id = cluster.worker.id;
132
+ const seconds = id * argv.spacing;
133
+ clearInterval(update);
134
+ log(process.title, 'Shutdown for Ctrl+C in', seconds, 'seconds.');
135
+ const health = network.peer.health();
136
+ const inRegionTags = network.inRegionConnections();
137
+ network.info(`Node ${id} has ${inRegionTags.length} connections in region.`);
138
+ network.info(network.ice(inRegionTags));
139
+ await delay(seconds * 1e3);
125
140
  await network.disconnect();
126
141
  process.exit(0);
127
- });
142
+ });
128
143
  await network.host();
129
-
130
144
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "uswest/80": "37.4852,-122.2364",
2
+ "uswest/80": "37,-122",
3
3
  "useast/89": "40,-75",
4
4
  "uscentle/88": "35,-83",
5
5
  "easteu/47": "50,17",
@@ -0,0 +1,46 @@
1
+ // Offer a websocket connection for testing, that skips the DHT entirely, and accepts publishing by pushing to subscribers over the websocket.
2
+ import { WebSocketServer } from 'ws';
3
+ import * as operator from '../public/javascripts/pubsub.js';
4
+
5
+ const sockets = {};
6
+ operator.setReceiver((nodeId, id, envelope) => {
7
+ const socket = sockets[nodeId];
8
+ socket.send(JSON.stringify([id, envelope]));
9
+ });
10
+
11
+ function heartbeat() {
12
+ this.isAlive = true;
13
+ }
14
+
15
+ export function configureWebsocket(server) {
16
+ const wss = new WebSocketServer({ server });
17
+ wss.on('connection', (ws, req) => {
18
+ const nodeTag = req.url.slice(1);
19
+ console.log('Connected', nodeTag);
20
+ sockets[nodeTag] = ws;
21
+
22
+ ws.on('message', async message => {
23
+ const [id, methodName, ...rest] = JSON.parse(message);
24
+ const result = await operator[methodName](...rest);
25
+ ws.send(JSON.stringify([id, result]));
26
+ });
27
+
28
+ ws.isAlive = true;
29
+ ws.on('pong', heartbeat);
30
+
31
+ ws.on('error', console.error);
32
+ ws.on('close', () => {
33
+ console.log('Disconnected', nodeTag);
34
+ operator.deleteSubscriber(nodeTag);
35
+ });
36
+ });
37
+
38
+ const interval = setInterval(function ping() { // Keep-alive ping/pong on interval
39
+ wss.clients.forEach(function each(ws) {
40
+ if (!ws.isAlive) return ws.terminate();
41
+ ws.isAlive = false;
42
+ ws.ping();
43
+ return null;
44
+ });
45
+ }, 30e3);
46
+ }
package/spec/axonSpec.js CHANGED
@@ -19,9 +19,7 @@
19
19
  Alice kills her publication and Emma joins.
20
20
  */
21
21
  const { describe, it, expect, beforeAll, afterAll, BigInt } = globalThis;
22
- import { createAuthorIdentity, regionCenter, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, deriveTopicId, metricTopic } from '@axona/protocol';
23
- import { connect } from '@axona/protocol/connect.js';
24
- globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
22
+ import { connect, createAuthorIdentity, regionCenter, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, deriveTopicId, metricTopic } from '@axona/protocol';
25
23
 
26
24
  class Node { // Stuff we have to do every time.
27
25
  static version = KERNEL_VERSION;
@@ -110,6 +108,7 @@ describe("CivilDefense", function () {
110
108
  const deliveryAllowanceMS = 60e3;
111
109
 
112
110
  //const location = regionCenter('uscentlw');
111
+ //const location = regionCenter('0x52');
113
112
  const location = regionCenter('uswest'); // TODO: pick one
114
113
 
115
114
  let alice, bob, carol, david, emma;
@@ -226,10 +225,12 @@ describe("CivilDefense", function () {
226
225
  });
227
226
  });
228
227
  afterAll(async function () {
229
- await alice.disconnect();
230
- await bob.disconnect();
231
- await carol?.disconnect();
232
- await david?.disconnect();
233
- await emma?.disconnect();
228
+ await Promise.all([
229
+ alice.disconnect(),
230
+ bob.disconnect(),
231
+ carol?.disconnect(),
232
+ david?.disconnect(),
233
+ emma?.disconnect(),
234
+ ]);
234
235
  });
235
236
  });
package/public/junk.html DELETED
@@ -1,145 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Material Web Autocomplete Combo Box</title>
6
- <script type="importmap">
7
- {
8
- "imports": {
9
- "uuid": "https://unpkg.com/uuid@13.0.0/dist/index.js",
10
- "@material/web/": "https://esm.run/@material/web/",
11
- "s2js": "./s2js/s2js.esm.js",
12
- "bigfloat": "./bigfloat/esm/index.js",
13
- "leaflet": "./leaflet/leaflet-src.esm.js",
14
- "minidenticons": "./minidenticons/minidenticons.min.js",
15
- "@axona/protocol": "./axona-protocol/src/index.js",
16
- "@axona/protocol/std": "./axona-protocol/std/index.js",
17
- "@axona/protocol/connect.js": "./axona-protocol/src/connect.js"
18
- }
19
- }
20
- </script>
21
- <script type="module">
22
- import '@material/web/all.js';
23
- import {styles as typescaleStyles} from '@material/web/typography/md-typescale-styles.js';
24
- document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
25
- </script>
26
- <!-- Load Material Web Components via CDN -->
27
- <!-- <script type="module"> -->
28
- <!-- import '@material/web/textfield/filled-text-field.js'; -->
29
- <!-- import '@material/web/menu/menu.js'; -->
30
- <!-- import '@material/web/menu/menu-item.js'; -->
31
- <!-- </script> -->
32
- <style>
33
- .combo-box-container {
34
- position: relative;
35
- display: inline-block;
36
- width: 300px;
37
- margin: 40px;
38
- }
39
- md-filled-text-field {
40
- width: 100%;
41
- }
42
- md-menu {
43
- /* Match the width of the textfield anchor */
44
- --md-menu-container-width: 300px;
45
- }
46
- </style>
47
- </head>
48
- <body>
49
-
50
- <div class="combo-box-container">
51
- <!-- The editable text input acting as the anchor -->
52
- <md-filled-text-field
53
- id="combo-input"
54
- label="Choose a fruit"
55
- placeholder="Type to search..."
56
- autocomplete="off">
57
- </md-filled-text-field>
58
-
59
- <!-- The dropdown menu containing autocomplete suggestions -->
60
- <md-menu id="combo-menu" anchor="combo-input" stay-open-on-outside-click>
61
- <!-- Populated dynamically via JavaScript -->
62
- </md-menu>
63
- </div>
64
-
65
- <script>
66
- const input = document.getElementById('combo-input');
67
- const menu = document.getElementById('combo-menu');
68
-
69
- // Dataset for autocomplete
70
- const itemsList = [
71
- 'Apple', 'Banana', 'Blueberry', 'Cherry', 'Grape',
72
- 'Lemon', 'Mango', 'Orange', 'Peach', 'Strawberry'
73
- ];
74
-
75
- // Build and filter menu items based on input value
76
- function updateMenu(filterText = '') {
77
- const normalizedFilter = filterText.toLowerCase().trim();
78
-
79
- // Filter list items
80
- const filtered = itemsList.filter(item =>
81
- item.toLowerCase().includes(normalizedFilter)
82
- );
83
-
84
- // Clear existing menu items
85
- menu.innerHTML = '';
86
-
87
- if (filtered.length === 0) {
88
- // Show a disabled helper item if no match found
89
- const noResult = document.createElement('md-menu-item');
90
- noResult.disabled = true;
91
- //noResult.headline = 'No matches found';
92
- noResult.textContent = 'No matches found';
93
- menu.appendChild(noResult);
94
- return;
95
- }
96
-
97
- // Append matched options
98
- filtered.forEach(item => {
99
- const menuItem = document.createElement('md-menu-item');
100
- //menuItem.headline = item;
101
- menuItem.textContent = item;
102
- // Store raw value on the element for extraction during selection
103
- menuItem.dataset.value = item;
104
- menu.appendChild(menuItem);
105
- });
106
- }
107
-
108
- // Filter list and open menu as user types
109
- input.addEventListener('input', (e) => {
110
- updateMenu(e.target.value);
111
- if (!menu.open) {
112
- menu.open = true;
113
- }
114
- });
115
-
116
- // Re-open full menu list when user clicks or focuses inside the input
117
- input.addEventListener('focus', () => {
118
- updateMenu(input.value);
119
- menu.open = true;
120
- });
121
-
122
- // Handle selection event from the Material menu
123
- menu.addEventListener('close-menu', (e) => {
124
- // e.detail.item holds the clicked md-menu-item instance
125
- const selectedItem = e.detail.item;
126
- if (selectedItem && selectedItem.dataset.value) {
127
- input.value = selectedItem.dataset.value;
128
- // Optionally trigger a change event for form tracking
129
- input.dispatchEvent(new Event('change'));
130
- }
131
- });
132
-
133
- // Close the dropdown cleanly if the user hits "Escape" or "Enter" inside input
134
- input.addEventListener('keydown', (e) => {
135
- if (e.key === 'Escape' || e.key === 'Enter') {
136
- menu.open = false;
137
- if(e.key === 'Enter') input.blur();
138
- }
139
- });
140
-
141
- // Initial population of the menu
142
- updateMenu();
143
- </script>
144
- </body>
145
- </html>