@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/index.js +2 -1
- package/package.json +6 -3
- package/public/images/loading.gif +0 -0
- package/public/index.html +6 -6
- package/public/javascripts/agent.js +72 -48
- package/public/javascripts/alert.js +126 -63
- package/public/javascripts/hashtags.js +148 -92
- package/public/javascripts/main.js +9 -5
- package/public/javascripts/map.js +1 -1
- package/public/javascripts/p2pWebNetwork.js +41 -16
- package/public/javascripts/protocol.js +168 -0
- package/public/javascripts/pubsub.js +124 -0
- package/public/javascripts/s2.js +36 -41
- package/public/javascripts/service-manager.js +5 -4
- package/public/javascripts/translations.js +1 -0
- package/public/javascripts/versions.js +16 -5
- package/public/service-worker.js +25 -10
- package/public/stylesheets/style.css +20 -13
- package/server/app.js +42 -28
- package/server/{location.json → location.save.json} +1 -1
- package/server/websocket.js +46 -0
- package/spec/axonSpec.js +9 -8
- package/public/junk.html +0 -145
- package/public/material-combobox.html +0 -379
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.
|
|
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
|
|
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('
|
|
35
|
+
.option('spacing', {
|
|
40
36
|
type: 'number',
|
|
41
|
-
default:
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
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:
|
|
114
|
-
debugLogger:
|
|
123
|
+
infoLogger: log,
|
|
124
|
+
debugLogger: debug
|
|
115
125
|
});
|
|
116
126
|
process.title = 'axona-' + network.nodeIdentity.id;
|
|
117
|
-
let update = setInterval(() => {
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
}
|
|
@@ -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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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>
|