@yz-social/civildefense.io 4.5.6 → 4.5.8
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/package.json +2 -2
- package/public/about/en.html +10 -1
- package/public/about/es.html +10 -1
- package/public/about/prevail.jpg +0 -0
- package/public/about/serving-you-high.png +0 -0
- package/public/about/serving-you.jpg +0 -0
- package/public/javascripts/alert.js +68 -31
- package/public/javascripts/conversation.js +3 -3
- package/public/javascripts/main.js +1 -1
- package/public/javascripts/map.js +5 -6
- package/public/javascripts/p2pWebNetwork.js +18 -1
- package/public/javascripts/translations.js +1 -1
- package/public/junk.html +145 -0
- package/public/service-worker.js +97 -94
- package/server/app.js +19 -5
- package/spec/conversationSpec.js +44 -38
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yz-social/civildefense.io",
|
|
3
3
|
"description": "Browser app to safely share live sightings on a map.",
|
|
4
|
-
"version": "4.5.
|
|
4
|
+
"version": "4.5.8",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"map",
|
|
7
7
|
"browser",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
".": "./index.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@axona/protocol": "github:axona-net/axona-protocol#
|
|
20
|
+
"@axona/protocol": "github:axona-net/axona-protocol#semver:^4.29.0",
|
|
21
21
|
"cors": "^2.8.6",
|
|
22
22
|
"express": "^4.22.1",
|
|
23
23
|
"leaflet": "^1.9.4",
|
package/public/about/en.html
CHANGED
|
@@ -132,12 +132,21 @@
|
|
|
132
132
|
<p>Axona is a modern descendant of the old <a href="https://en.wikipedia.org/wiki/Peer-to-peer" target="yz.other" rel="noopener">peer-to-peer networks</a> such as <a href="https://en.wikipedia.org/wiki/Napster">Napster</a>. It has a unique self-healing routing mechanism that uses neuron learning techniques to find the best way to distribute alerts. It is analogous to <a href="https://en.wikipedia.org/wiki/Tor_(network)" target="yz.other" rel="noopener">TOR</a> (The Onion Router), but designed from the ground up to run in mobile and desktop browsers.</p>
|
|
133
133
|
<p>We are also developing a comprehensive quantum-ready cryptography system that will add private-groups for Civil Defense and any software on the network. We believe there are many applications where developers, users, and the public all benefit from open software and secure, decentralized networks — without app stores, servers, or data centers. Beyond Civil Defense, we are also developing large-scale <a href="https://en.wikipedia.org/wiki/Distributed_computing" target="yz.other" rel="noopener">multiuser collaboration</a> and <a href="https://poverty-action.org/inclusive-instant-payment-systems-evidence-based-approach-design-impact" target="yz.other" rel="noopener">Inclusive Instant Payment Systems</a>.</p>
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
|
|
136
|
+
<div class="centered">
|
|
137
|
+
<img src="serving-you.jpg" alt="Civil Defense Week poster - Serving you in time of emergency">
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="centered">
|
|
141
|
+
<img src="prevail.jpg" alt="Cold-War Civil Defense poster - Prepare, Protect, Prevail">
|
|
142
|
+
</div>
|
|
136
143
|
|
|
137
144
|
<div class="centered">
|
|
138
145
|
<img src="nazis.png" alt="WWII-style poster exhorting the public to use Civil Defense in case of Nazi attack.">
|
|
139
146
|
</div>
|
|
140
147
|
|
|
148
|
+
</main>
|
|
149
|
+
|
|
141
150
|
<!-- ── FOOTER ─────────────────────────────────────────────────── -->
|
|
142
151
|
<footer>
|
|
143
152
|
<p><a href="/?lang=en" target="civildefense">civildefense.io</a> — Safe and free crowdmapping for vulnerable populations.<br>
|
package/public/about/es.html
CHANGED
|
@@ -117,12 +117,21 @@
|
|
|
117
117
|
<p>Axona es un descendiente moderno de las antiguas <a href="https://en.wikipedia.org/wiki/Peer-to-peer" target="yz.other" rel="noopener">redes entre pares</a> como <a href="https://en.wikipedia.org/wiki/Napster">Napster</a>. Tiene un mecanismo único de enrutamiento autocurativo que utiliza técnicas de aprendizaje neuronal para encontrar la mejor manera de distribuir alertas. Es análogo a <a href="https://en.wikipedia.org/wiki/Tor_(network)" target="yz.other" rel="noopener">TOR</a> (The Onion Router), pero diseñado desde cero para funcionar en navegadores móviles y de escritorio.</p>
|
|
118
118
|
<p>También estamos desarrollando un sistema integral de criptografía preparado para la era cuántica que añadirá grupos privados para Defensa Civil y cualquier software en la red. Creemos que hay muchas aplicaciones en las que los desarrolladores, los usuarios y el público en general se benefician del software abierto y las redes seguras y descentralizadas, sin tiendas de aplicaciones, servidores ni centros de datos. Más allá de Defensa Civil, también estamos desarrollando <a href="https://en.wikipedia.org/wiki/Distributed_computing" target="yz.other" rel="noopener">colaboración multiusuario a gran escala</a> y <a href="https://poverty-action.org/inclusive-instant-payment-systems-evidence-based-approach-design-impact" target="yz.other" rel="noopener">Sistemas de Pago Instantáneo Inclusivos</a>.</p>
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
<div class="centered">
|
|
121
|
+
<img src="serving-you.jpg" alt="Cartel de la Semana de la Defensa Civil: A su servicio en caso de emergencia">
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="centered">
|
|
125
|
+
<img src="prevail.jpg" alt="Póster de Defensa Civil de la Guerra Fría: Preparar, proteger, prevalecer">
|
|
126
|
+
</div>
|
|
121
127
|
|
|
122
128
|
<div class="centered">
|
|
123
129
|
<img src="nazis.png" alt="Póster de estilo de la Segunda Guerra Mundial que exhorta al público a usar Defensa Civil en caso de ataque nazi.">
|
|
124
130
|
</div>
|
|
125
131
|
|
|
132
|
+
</main>
|
|
133
|
+
|
|
134
|
+
|
|
126
135
|
<!-- ── FOOTER ─────────────────────────────────────────────────── -->
|
|
127
136
|
<footer>
|
|
128
137
|
<p><a href="/?lang=es" target="civildefense">civildefense.io</a> — Mapeo colaborativo seguro y gratuito para poblaciones vulnerables.<br>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,7 +6,7 @@ import { networkPromise, resetInactivityTimer, notificationsAllowed, tooltip, cl
|
|
|
6
6
|
import { consume } from './display.js';
|
|
7
7
|
import { Hashtags } from './hashtags.js';
|
|
8
8
|
import { Agent } from './agent.js';
|
|
9
|
-
import { Conversation } from './conversation.js';
|
|
9
|
+
import { Conversation, Reply } from './conversation.js';
|
|
10
10
|
import { alertTopic } from './versions.js';
|
|
11
11
|
import { getContainingCells, findCoverCellsByCenterAndPoint } from './s2.js';
|
|
12
12
|
const { localStorage, getComputedStyle, URL, URLSearchParams, domtoimage } = globalThis;
|
|
@@ -72,6 +72,37 @@ export function go({lat = null, lng = null, zoom = null, alert = null}) { // Go
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
class AlertReply extends Reply {
|
|
76
|
+
async initialize(properties) { // Set properties of this AlertReply.
|
|
77
|
+
// Currently, this waits for any attachment to be fetched. (Conversation.ensure() will wait as long as we need.)
|
|
78
|
+
// This means that the alert will not show up until we have the attachments, and then it shows up all at once with the text,
|
|
79
|
+
// attribution, and attachment. (There's no img or video "loading" period because the attachment comes through as a complete
|
|
80
|
+
// data url.) This is convenient for the formatReply function, because until the chunked attachment is re-assembled, we won't
|
|
81
|
+
// know which kind of player to use, and we won't be able to assign the containing A[href].
|
|
82
|
+
//
|
|
83
|
+
// Alternatively, we could in the future choose to have the attributions and text appear right away, with a "spinner"
|
|
84
|
+
// placeholder that turns into the appropriate A/player when the attachment resolves. To do that, we would need to record here
|
|
85
|
+
// a promise to reassemble the attachment, have formatReply create a findable placeholder to later replace with the A element
|
|
86
|
+
// and contents, and then have ensureContent() arrange in its delayed followup to replace the placeholder when the promise
|
|
87
|
+
// resolves.
|
|
88
|
+
super.initialize(properties);
|
|
89
|
+
const {container, agent, issuedTime, payload} = properties;
|
|
90
|
+
const {file:attachmentTopic} = payload;
|
|
91
|
+
if (attachmentTopic) {
|
|
92
|
+
const contact = await networkPromise;
|
|
93
|
+
// Before pushing data on to replies.
|
|
94
|
+
const {dataURL:file, name, msgIds} = await contact.assembleChunkedDataURL(attachmentTopic);
|
|
95
|
+
Object.assign(payload, {file, name, attachmentTopic, msgIds});
|
|
96
|
+
}
|
|
97
|
+
const element = container.startFader('.alert-commented', issuedTime + ttl - Date.now());
|
|
98
|
+
element.style.display = 'block';
|
|
99
|
+
// Restart the pulse animation by setting animationName to something it isn't.
|
|
100
|
+
element.style.animationName = element.style.animationName === 'pulse2' ? 'pulse' : 'pulse2';
|
|
101
|
+
container.showNotification({agent, issuedTime, body: payload.message || payload.name || payload});
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
let subscriptions = []; // array of stringy keys <mumble>:<cellID>:<hashtag>
|
|
76
107
|
let subscriptionsRegion;
|
|
77
108
|
// We do not record exactly where you were looking across sessions, but we do record the containing level 9 cell.
|
|
@@ -129,7 +160,7 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
129
160
|
// However, the 'unpublishing' (if any) is invoked first.
|
|
130
161
|
// To do this, we must hash the eventName ourselves.
|
|
131
162
|
//console.log('publish', {lat, lng, hashtag, payload, cancel, subject, issuedTime, rest});
|
|
132
|
-
if (publishing) { console.log('skiping
|
|
163
|
+
if (publishing) { console.log('skiping overlapping publish'); return null; } // do not stack them up.
|
|
133
164
|
try {
|
|
134
165
|
publishing = true;
|
|
135
166
|
|
|
@@ -230,8 +261,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
230
261
|
wrapper.initChangeHashtag(popupAttribution);
|
|
231
262
|
});
|
|
232
263
|
}
|
|
233
|
-
static ensure({tag, topic, ts, issuedTime, ...rest}) { // Add marker at position with appropriate fade if not already present.
|
|
234
|
-
const alert = super.ensure({tag, subject: tag, issuedTime, ...rest}); // Does not include topic or ts. fixme: get rid of subject. fixme: why is ts different?
|
|
264
|
+
static async ensure({tag, topic, ts, issuedTime, ...rest}) { // Add marker at position with appropriate fade if not already present.
|
|
265
|
+
const alert = await super.ensure({tag, subject: tag, issuedTime, ...rest}); // Does not include topic or ts. fixme: get rid of subject. fixme: why is ts different?
|
|
235
266
|
if (!alert) return null;
|
|
236
267
|
// Regardless of initialize vs update, reset fader.
|
|
237
268
|
const now = Date.now(),
|
|
@@ -312,7 +343,8 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
312
343
|
this.initChangeHashtag(popupElement);
|
|
313
344
|
for (const correspondent of popupElement.querySelectorAll('.correspondent')) {
|
|
314
345
|
const tag = correspondent.dataset.tag;
|
|
315
|
-
const
|
|
346
|
+
const region = P2PWebNetwork.regionCode(this.lat, this.lng);
|
|
347
|
+
const agent = Agent.ensure({tag, region: region});
|
|
316
348
|
const isAvatar = correspondent.classList.contains('avatar');
|
|
317
349
|
if (agent.addElement(correspondent, 'mixed', isAvatar ? 'avatar' : 'handle')) {
|
|
318
350
|
const isMine = Agent.isMine(tag);
|
|
@@ -408,28 +440,15 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
408
440
|
}
|
|
409
441
|
|
|
410
442
|
// Each reply is separately published by its author, and only they can modify/unpublish it.
|
|
443
|
+
get itemKind() { // Answer class of reply items.
|
|
444
|
+
return AlertReply;
|
|
445
|
+
}
|
|
411
446
|
async ensure(data) { // Add or update reply for this marker.
|
|
412
447
|
data.subject = data.tag; //fixme
|
|
413
|
-
const reply = super.ensure(data);
|
|
414
|
-
if (reply) { // TODO? Move to AlertReply.initialize()?
|
|
415
|
-
const {agent, issuedTime, payload} = data;
|
|
416
|
-
const {file} = payload;
|
|
417
|
-
if (file) {
|
|
418
|
-
data.fileTopic = file;
|
|
419
|
-
const contact = await networkPromise;
|
|
420
|
-
// Before pushing data on to replies.
|
|
421
|
-
const {dataURL, name} = await contact.assembleChunkedDataURL(file);
|
|
422
|
-
payload.file = dataURL;
|
|
423
|
-
payload.name = name;
|
|
424
|
-
}
|
|
425
|
-
const element = this.startFader('.alert-commented', issuedTime + ttl - Date.now());
|
|
426
|
-
element.style.display = 'block';
|
|
427
|
-
// Restart the pulse animation by setting animationName to something it isn't.
|
|
428
|
-
element.style.animationName = element.style.animationName === 'pulse2' ? 'pulse' : 'pulse2';
|
|
429
|
-
this.showNotification({agent, issuedTime, body: payload.message || payload.name || payload});
|
|
430
|
-
}
|
|
448
|
+
const reply = await super.ensure(data);
|
|
431
449
|
this.needsRedisplay = true;
|
|
432
450
|
this.ensureContent();
|
|
451
|
+
return reply;
|
|
433
452
|
}
|
|
434
453
|
async postReply(event) { // Post a reply to this marker's subject, in response to a text-field change event.
|
|
435
454
|
resetInactivityTimer();
|
|
@@ -437,14 +456,15 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
437
456
|
const button = event.target;
|
|
438
457
|
const inputElement = button.parentElement;
|
|
439
458
|
let payload = inputElement.value.trim();
|
|
440
|
-
const {subject, hashtag,
|
|
459
|
+
const {subject, hashtag, lat, lng} = this;
|
|
460
|
+
const region = P2PWebNetwork.regionCode(lat, lng);
|
|
441
461
|
const files = inputElement.parentElement.querySelector('input[type="file"]').files;
|
|
442
462
|
if (!payload && !files.length) return;
|
|
443
463
|
inputElement.value = '';
|
|
444
464
|
inputElement.querySelector('md-filled-icon-button').toggleAttribute('disabled', true);
|
|
445
465
|
const contact = await networkPromise;
|
|
446
466
|
if (files.length) {
|
|
447
|
-
const {topic:file} = await contact.chunkifyBlob({blob: files[0], region});
|
|
467
|
+
const {topic:file, msgIds} = await contact.chunkifyBlob({blob: files[0], region});
|
|
448
468
|
payload = {message: payload, file};
|
|
449
469
|
}
|
|
450
470
|
await contact.publish({eventName: subject, region, payload}); // Publish the new reply.
|
|
@@ -452,8 +472,22 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
452
472
|
}
|
|
453
473
|
deleteReply(replyElement) {
|
|
454
474
|
resetInactivityTimer();
|
|
455
|
-
const {
|
|
456
|
-
|
|
475
|
+
const {lat, lng} = this;
|
|
476
|
+
const region = P2PWebNetwork.regionCode(lat, lng);
|
|
477
|
+
const killTag = replyElement.dataset.subject;
|
|
478
|
+
networkPromise.then(async contact => {
|
|
479
|
+
// We won't be here unless we are the signer.
|
|
480
|
+
await contact.publish({eventName: this.subject, region, killTag, payload: null});
|
|
481
|
+
// IFF there's an attachment AND we're given msgIds by receiveChunkedBytes, then delete the attachment.
|
|
482
|
+
const reply = this.getItem(killTag);
|
|
483
|
+
const {attachmentTopic, msgIds = []} = reply?.payload || {};
|
|
484
|
+
if (msgIds.length) {
|
|
485
|
+
const {name, owner, region} = attachmentTopic;
|
|
486
|
+
for (const killTag of msgIds) {
|
|
487
|
+
await contact.publish({eventName: name, region, owner, killTag, payload: null});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
});
|
|
457
491
|
}
|
|
458
492
|
showNotification({issuedTime = this.issuedTime, body = '', agent = this.agent, alert = this.subject, lat = this.lat, lng = this.lng, hashtag = this.hashtag}) {
|
|
459
493
|
// Give OS notification that comes back to here, unless act is us.
|
|
@@ -469,18 +503,21 @@ export class Alert extends Conversation { // A wrapper around L.marker
|
|
|
469
503
|
registration.showNotification(hashtag, options);
|
|
470
504
|
});
|
|
471
505
|
}
|
|
506
|
+
// Each reply element is a DIV.reply with data-subject and data-text attributes that are used in sharing.
|
|
507
|
+
// It contains an attribution header with controls, zero or one attachments, and then the message text.
|
|
508
|
+
// If present the attachment will be an A element with download attribute, surrounding either an IMG, A/V player, or an attachment icon followed by the file name.
|
|
472
509
|
formatReplies() { // Answer HTML for the replies and input box.
|
|
473
510
|
const { items, agent, originalPosting } = this;
|
|
474
511
|
const formatReply = ({subject, payload, ...rest}) => {
|
|
475
|
-
const {message = payload, file, name} = payload;
|
|
512
|
+
const {message = payload, file, name} = payload || {}; // Message text converts recognized urls to A/V players or links.
|
|
476
513
|
let text = message
|
|
477
514
|
.replace(/https?:\/\/\S+\.(mp3|aac|ogg|oga|opus|m4a|m3u8|m3u|mpu|mpd)$/ig, url => `<audio controls src="${url}"></audio>`) // show audio urls as players
|
|
478
515
|
.replace(/https?:\/\/\S+\.(mp4|mov|webm)$/ig, url => `<video controls src="${url}"></video>`) // show video urls as players
|
|
479
516
|
.replace(/(?<!")https?:\/\/\S+/g, url => `<a href="${url}" target="yz.sidebar">${url}</a>`); // show urls as links
|
|
480
517
|
let attachment = '';
|
|
481
|
-
if (file?.startsWith('data:image')) attachment = `<a href="${file}" download="${name}"><img class="attachment" src="${file}"></img></a>`;
|
|
482
|
-
else if (file?.startsWith('data:audio')) attachment = `<a href="${file}" download="${name}"><audio controls class="attachment" src="${file}"></audio></a>`;
|
|
483
|
-
else if (file?.startsWith('data:video')) attachment = `<a href="${file}" download="${name}"><video controls class="attachment" src="${file}"></video></a>`;
|
|
518
|
+
if (file?.startsWith?.('data:image')) attachment = `<a href="${file}" download="${name}"><img class="attachment" src="${file}"></img></a>`;
|
|
519
|
+
else if (file?.startsWith?.('data:audio')) attachment = `<a href="${file}" download="${name}"><audio controls class="attachment" src="${file}"></audio></a>`;
|
|
520
|
+
else if (file?.startsWith?.('data:video')) attachment = `<a href="${file}" download="${name}"><video controls class="attachment" src="${file}"></video></a>`;
|
|
484
521
|
else if (file) attachment = `
|
|
485
522
|
<div class="attachment file">
|
|
486
523
|
<a href="${file}" download="${name}">
|
|
@@ -29,12 +29,12 @@ export class Tagged { // Maintains cached existence within a (possibly instance-
|
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
static ensureIn(data, container, kind = container.itemKind) { // update() or initialize() item and remember what those answer. (Falsy is deleted).
|
|
32
|
+
static async ensureIn(data, container, kind = container.itemKind) { // update() or initialize() item and remember what those answer. (Falsy is deleted).
|
|
33
33
|
const {tag, payload, ...rest} = data;
|
|
34
34
|
let item = container.getItem(tag);
|
|
35
35
|
if (!payload) return item && container.removeItem(tag)?.destroy();
|
|
36
|
-
if (item) item = item.update(data);
|
|
37
|
-
else item = new kind().initialize(data);
|
|
36
|
+
if (item) item = await item.update(data);
|
|
37
|
+
else item = await (new kind().initialize({...data, container}));
|
|
38
38
|
|
|
39
39
|
if (!item) return container.removeItem(tag)?.destroy();
|
|
40
40
|
return container.setItem(tag, item);
|
|
@@ -123,7 +123,7 @@ export function openAbout(event) {
|
|
|
123
123
|
openDisplay('aboutContainer', event);
|
|
124
124
|
noteNotificationPermission(window.Notification?.permission);
|
|
125
125
|
}
|
|
126
|
-
clickTip('#aboutButton', Int`Information about this app, and options to change how you appear to others.`, event => { // open about
|
|
126
|
+
clickTip('#aboutButton', Int`Information about this app, and options to change notifications or how you appear to others.`, event => { // open about
|
|
127
127
|
Alert.closePopup();
|
|
128
128
|
openAbout(event);
|
|
129
129
|
});
|
|
@@ -43,18 +43,17 @@ export function updateLocation(lat, lng, zoom, positionLabel) { // initMap if ne
|
|
|
43
43
|
if (!map) {
|
|
44
44
|
initMap(lat, lng, zoom, positionLabel);
|
|
45
45
|
|
|
46
|
-
const
|
|
46
|
+
const url = new URL(location);
|
|
47
|
+
const params = url.searchParams;
|
|
47
48
|
const tags = params.get('tags');
|
|
48
49
|
const tagsArray = tags?.split(',') || [];
|
|
49
50
|
tagsArray.forEach(tag => Hashtags.add(decodeURIComponent(tag)));
|
|
50
51
|
Hashtags.onchange({resetSubscriptions: false}); // Too early to subscribe, but will be done during initialization.
|
|
51
52
|
go({lat: params.get('lat'), lng: params.get('lng'), zoom: params.get('z'), alert: params.get('alert')});
|
|
52
53
|
// We don't need the query parameters now. Get rid of them. They're annoying.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
copy.search = '';
|
|
57
|
-
history.replaceState(null, '', copy);
|
|
54
|
+
if (params.size > 0) {
|
|
55
|
+
['tags', 'lat', 'lng', 'z', 'alert'].forEach(key => params.delete(key));
|
|
56
|
+
history.replaceState(null, '', url);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
return;
|
|
@@ -2,6 +2,13 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
2
2
|
import { createNodeIdentity, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION } from '@axona/protocol';
|
|
3
3
|
import { stringToBytes, bytesToString, publishChunkedBytes, receiveChunkedBytes } from '@axona/protocol/std';
|
|
4
4
|
import { connect } from '@axona/protocol/connect.js';
|
|
5
|
+
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection); // To support @axona/protocol < 4.26
|
|
6
|
+
if (!Uint8Array.prototype.toBase64) { // NodeJS < 24
|
|
7
|
+
Object.defineProperty(Uint8Array.prototype, 'toBase64', {
|
|
8
|
+
value: function toBase64() { return Buffer.from(this.buffer, this.byteOffset, this.byteLength).toString('base64'); },
|
|
9
|
+
writable: true, configurable: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
5
12
|
const { BigInt, URL, File, pica } = globalThis;
|
|
6
13
|
|
|
7
14
|
/* Example:
|
|
@@ -20,7 +27,9 @@ export class P2PWebNetwork {
|
|
|
20
27
|
static sessionRegion = sessionRegionPromise;
|
|
21
28
|
static async create({infoLogger = console.log, debugLogger,
|
|
22
29
|
region = this.sessionRegion,
|
|
23
|
-
bridgeUrl = globalThis.
|
|
30
|
+
bridgeUrl = (globalThis.location && new URL(globalThis.location).searchParams.get('bridge')) ||
|
|
31
|
+
globalThis.process?.env.BRIDGE_URL ||
|
|
32
|
+
'wss://bridge.axona.net',
|
|
24
33
|
} = {}) {
|
|
25
34
|
// Promise a ready-to-use network peer.
|
|
26
35
|
region = await region;
|
|
@@ -35,6 +44,14 @@ export class P2PWebNetwork {
|
|
|
35
44
|
Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer});
|
|
36
45
|
network.resetStatePromises();
|
|
37
46
|
network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(region.lat, region.lng).toString(16)}.`);
|
|
47
|
+
peer.onError(error => {
|
|
48
|
+
network.info(`error: ${error.message || error}`);
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
//peer.onLog('debug', (...rest) => network.debug('DEBUG', ...rest));
|
|
52
|
+
//peer.onLog('info', (...rest) => network.debug('INFO', ...rest));
|
|
53
|
+
peer.onLog('warn', (...rest) => network.info('WARNING', ...rest));
|
|
54
|
+
peer.onLog('error', (...rest) => network.info('ERROR', ...rest));
|
|
38
55
|
const { peers, ms } = status;
|
|
39
56
|
network.info(`Connected ${peers} connections through ${bridgeUrl} in ${ms.toLocaleString()} ms.`);
|
|
40
57
|
network.attached(network);
|
|
@@ -106,7 +106,7 @@ const translations = {
|
|
|
106
106
|
['Change the image shown for you in the conversation for your alerts and replies.']: {es: "Cambia la imagen que se muestra en la conversación para tus alertas y respuestas."},
|
|
107
107
|
['Toggle whether alerts for this topic shown on the map. Separately, a radio button is shown when selected and sets this as the initial topic of the next alert you make, while an x button is shown when deselected and removes the topic.']: {es: "Alterna la visualización en el mapa de las alertas relacionadas con este tema. Por otro lado, al seleccionar el tema aparece un botón de opción que lo establece como el tema inicial de la próxima alerta que crees; al deseleccionarlo, aparece un botón con una «x» que elimina dicho tema."},
|
|
108
108
|
['Add a new topic for which the map should show any alerts.']: {es: "Añada un nuevo tema para el cual el mapa deba mostrar alertas."},
|
|
109
|
-
['Information about this app, and options to change how you appear to others.']: {es: "Información sobre esta aplicación y opciones para
|
|
109
|
+
['Information about this app, and options to change notifications or how you appear to others.']: {es: "Información sobre esta aplicación y opciones para modificar las notificaciones o cómo apareces ante los demás."},
|
|
110
110
|
['Wipe from ']: {es: "Elimina de "},
|
|
111
111
|
[' all personal data and source files for this app.']: {es: " todos los datos personales y archivos fuente de esta aplicación."},
|
|
112
112
|
['Display QR code that can be scanned by a friend to show the same map on their device.']: {es: "Muestra un código QR que un amigo puede escanear para ver el mismo mapa en su dispositivo."},
|
package/public/junk.html
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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>
|
package/public/service-worker.js
CHANGED
|
@@ -1,6 +1,100 @@
|
|
|
1
1
|
const { Request, Response, URL, clients} = self;
|
|
2
2
|
// Little point in trying to automatically pull this through package.json, as we need a byte change in THIS file to trigger a new worker.
|
|
3
|
-
const serviceVersion = '4.5.
|
|
3
|
+
const serviceVersion = '4.5.8';
|
|
4
|
+
|
|
5
|
+
const cacheList = [ // The files we need.
|
|
6
|
+
"/",
|
|
7
|
+
"/index.html", // Just in case anyone is specifying that.
|
|
8
|
+
"favicon.ico",
|
|
9
|
+
"manifest.json",
|
|
10
|
+
"package.json",
|
|
11
|
+
|
|
12
|
+
"javascripts/versions.js",
|
|
13
|
+
"javascripts/main.js",
|
|
14
|
+
"javascripts/display.js",
|
|
15
|
+
"javascripts/conversation.js",
|
|
16
|
+
"javascripts/alert.js",
|
|
17
|
+
"javascripts/map.js",
|
|
18
|
+
"javascripts/hashtags.js",
|
|
19
|
+
"javascripts/s2.js",
|
|
20
|
+
"javascripts/agent.js",
|
|
21
|
+
"javascripts/translations.js",
|
|
22
|
+
"javascripts/service-manager.js",
|
|
23
|
+
"javascripts/p2pWebNetwork.js",
|
|
24
|
+
|
|
25
|
+
"stylesheets/style.css",
|
|
26
|
+
|
|
27
|
+
"images/qr-scan.svg",
|
|
28
|
+
"images/share.svg",
|
|
29
|
+
"images/recenter.svg",
|
|
30
|
+
"images/civil-defense-122.png",
|
|
31
|
+
"images/civil-defense-192.png",
|
|
32
|
+
"images/civil-defense-240.png",
|
|
33
|
+
"images/civil-defense-512.png",
|
|
34
|
+
|
|
35
|
+
"axona-protocol/src/index.js",
|
|
36
|
+
"axona-protocol/src/errors.js",
|
|
37
|
+
"axona-protocol/src/bridgeDirectory.js",
|
|
38
|
+
"axona-protocol/src/contracts/Transport.js",
|
|
39
|
+
"axona-protocol/src/contracts/DHT.js",
|
|
40
|
+
"axona-protocol/src/contracts/BootstrapService.js",
|
|
41
|
+
"axona-protocol/src/identity/index.js",
|
|
42
|
+
"axona-protocol/src/identity/nodeid.js",
|
|
43
|
+
"axona-protocol/src/pow/pow.js",
|
|
44
|
+
"axona-protocol/src/dht/AxonaPeer.js",
|
|
45
|
+
"axona-protocol/src/dht/AxonaDomain.js",
|
|
46
|
+
"axona-protocol/src/dht/DHTNode.js",
|
|
47
|
+
"axona-protocol/src/dht/NeuronNode.js",
|
|
48
|
+
"axona-protocol/src/dht/Synapse.js",
|
|
49
|
+
"axona-protocol/src/dht/Subscription.js",
|
|
50
|
+
"axona-protocol/src/pubsub/AxonaManager.js",
|
|
51
|
+
"axona-protocol/src/pubsub/authorClass.js",
|
|
52
|
+
"axona-protocol/src/pubsub/kill.js",
|
|
53
|
+
"axona-protocol/src/pubsub/touch.js",
|
|
54
|
+
"axona-protocol/src/pubsub/post.js",
|
|
55
|
+
"axona-protocol/src/pubsub/unpub.js",
|
|
56
|
+
"axona-protocol/src/pubsub/envelope.js",
|
|
57
|
+
"axona-protocol/src/pubsub/ed25519.js",
|
|
58
|
+
"axona-protocol/src/pubsub/metrics.js",
|
|
59
|
+
"axona-protocol/src/utils/region-names.js",
|
|
60
|
+
"axona-protocol/src/utils/geo.js",
|
|
61
|
+
"axona-protocol/src/utils/s2.js",
|
|
62
|
+
"axona-protocol/src/utils/hexid.js",
|
|
63
|
+
"axona-protocol/src/transport/handshake.js",
|
|
64
|
+
"axona-protocol/src/transport/handshake-auth.js",
|
|
65
|
+
"axona-protocol/src/transport/wire.js",
|
|
66
|
+
"axona-protocol/src/transport/web/index.js",
|
|
67
|
+
"axona-protocol/src/transport/web/mesh.js",
|
|
68
|
+
"axona-protocol/src/transport/web/mesh-auth.js",
|
|
69
|
+
"axona-protocol/src/transport/web/webrtc.js",
|
|
70
|
+
"axona-protocol/src/transport/web/bridge.js",
|
|
71
|
+
"axona-protocol/src/transport/web/composite.js",
|
|
72
|
+
"axona-protocol/src/transport/sim/index.js",
|
|
73
|
+
"axona-protocol/src/transport/sim/network.js",
|
|
74
|
+
"axona-protocol/src/transport/sim/transport.js",
|
|
75
|
+
"axona-protocol/src/persistence/interface.js",
|
|
76
|
+
"axona-protocol/src/crypto/noble-ed25519.js",
|
|
77
|
+
"axona-protocol/std/index.js",
|
|
78
|
+
"axona-protocol/std/chunk.js",
|
|
79
|
+
|
|
80
|
+
"leaflet/leaflet.css",
|
|
81
|
+
"leaflet/leaflet-src.esm.js",
|
|
82
|
+
"leaflet/images/marker-icon.png",
|
|
83
|
+
"leaflet/images/marker-icon-2x.png",
|
|
84
|
+
"leaflet/images/marker-shadow.png",
|
|
85
|
+
|
|
86
|
+
"pica/pica.min.js",
|
|
87
|
+
"minidenticons/minidenticons.min.js",
|
|
88
|
+
"s2js/s2js.esm.js",
|
|
89
|
+
"bigfloat/esm/index.js",
|
|
90
|
+
"bigfloat/esm/BigFloat32",
|
|
91
|
+
"bigfloat/esm/BigFloat53",
|
|
92
|
+
"bigfloat/esm/BigComplex",
|
|
93
|
+
"bigfloat/esm/BaseInfo32",
|
|
94
|
+
"bigfloat/esm/util",
|
|
95
|
+
|
|
96
|
+
// TODO: the libraries
|
|
97
|
+
];
|
|
4
98
|
|
|
5
99
|
async function cacheFirst({request, event}) {
|
|
6
100
|
// Handle request from any cache, else fetch and store it in serviceCache.
|
|
@@ -72,99 +166,8 @@ self.addEventListener('fetch', event => {
|
|
|
72
166
|
async function cacheSource(version, event) { // Cache source in the given version.
|
|
73
167
|
console.log(`service-worker ${serviceVersion} is caching source in cache ${version}.`);
|
|
74
168
|
const cache = await caches.open(version);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"/index.html", // Just in case anyone is specifying that.
|
|
78
|
-
"favicon.ico",
|
|
79
|
-
"manifest.json",
|
|
80
|
-
"package.json",
|
|
81
|
-
|
|
82
|
-
"javascripts/versions.js",
|
|
83
|
-
"javascripts/main.js",
|
|
84
|
-
"javascripts/display.js",
|
|
85
|
-
"javascripts/conversation.js",
|
|
86
|
-
"javascripts/alert.js",
|
|
87
|
-
"javascripts/map.js",
|
|
88
|
-
"javascripts/hashtags.js",
|
|
89
|
-
"javascripts/s2.js",
|
|
90
|
-
"javascripts/agent.js",
|
|
91
|
-
"javascripts/translations.js",
|
|
92
|
-
"javascripts/service-manager.js",
|
|
93
|
-
"javascripts/p2pWebNetwork.js",
|
|
94
|
-
|
|
95
|
-
"stylesheets/style.css",
|
|
96
|
-
|
|
97
|
-
"images/qr-scan.svg",
|
|
98
|
-
"images/share.svg",
|
|
99
|
-
"images/recenter.svg",
|
|
100
|
-
"images/civil-defense-122.png",
|
|
101
|
-
"images/civil-defense-192.png",
|
|
102
|
-
"images/civil-defense-240.png",
|
|
103
|
-
"images/civil-defense-512.png",
|
|
104
|
-
|
|
105
|
-
"axona-protocol/src/index.js",
|
|
106
|
-
"axona-protocol/src/errors.js",
|
|
107
|
-
"axona-protocol/src/bridgeDirectory.js",
|
|
108
|
-
"axona-protocol/src/contracts/Transport.js",
|
|
109
|
-
"axona-protocol/src/contracts/DHT.js",
|
|
110
|
-
"axona-protocol/src/contracts/BootstrapService.js",
|
|
111
|
-
"axona-protocol/src/identity/index.js",
|
|
112
|
-
"axona-protocol/src/identity/nodeid.js",
|
|
113
|
-
"axona-protocol/src/pow/pow.js",
|
|
114
|
-
"axona-protocol/src/dht/AxonaPeer.js",
|
|
115
|
-
"axona-protocol/src/dht/AxonaDomain.js",
|
|
116
|
-
"axona-protocol/src/dht/DHTNode.js",
|
|
117
|
-
"axona-protocol/src/dht/NeuronNode.js",
|
|
118
|
-
"axona-protocol/src/dht/Synapse.js",
|
|
119
|
-
"axona-protocol/src/dht/Subscription.js",
|
|
120
|
-
"axona-protocol/src/pubsub/AxonaManager.js",
|
|
121
|
-
"axona-protocol/src/pubsub/authorClass.js",
|
|
122
|
-
"axona-protocol/src/pubsub/kill.js",
|
|
123
|
-
"axona-protocol/src/pubsub/touch.js",
|
|
124
|
-
"axona-protocol/src/pubsub/post.js",
|
|
125
|
-
"axona-protocol/src/pubsub/unpub.js",
|
|
126
|
-
"axona-protocol/src/pubsub/envelope.js",
|
|
127
|
-
"axona-protocol/src/pubsub/ed25519.js",
|
|
128
|
-
"axona-protocol/src/pubsub/metrics.js",
|
|
129
|
-
"axona-protocol/src/utils/region-names.js",
|
|
130
|
-
"axona-protocol/src/utils/geo.js",
|
|
131
|
-
"axona-protocol/src/utils/s2.js",
|
|
132
|
-
"axona-protocol/src/utils/hexid.js",
|
|
133
|
-
"axona-protocol/src/transport/handshake.js",
|
|
134
|
-
"axona-protocol/src/transport/handshake-auth.js",
|
|
135
|
-
"axona-protocol/src/transport/wire.js",
|
|
136
|
-
"axona-protocol/src/transport/web/index.js",
|
|
137
|
-
"axona-protocol/src/transport/web/mesh.js",
|
|
138
|
-
"axona-protocol/src/transport/web/mesh-auth.js",
|
|
139
|
-
"axona-protocol/src/transport/web/webrtc.js",
|
|
140
|
-
"axona-protocol/src/transport/web/bridge.js",
|
|
141
|
-
"axona-protocol/src/transport/web/composite.js",
|
|
142
|
-
"axona-protocol/src/transport/sim/index.js",
|
|
143
|
-
"axona-protocol/src/transport/sim/network.js",
|
|
144
|
-
"axona-protocol/src/transport/sim/transport.js",
|
|
145
|
-
"axona-protocol/src/persistence/interface.js",
|
|
146
|
-
"axona-protocol/src/crypto/noble-ed25519.js",
|
|
147
|
-
"axona-protocol/std/index.js",
|
|
148
|
-
"axona-protocol/std/chunk.js",
|
|
149
|
-
|
|
150
|
-
"leaflet/leaflet.css",
|
|
151
|
-
"leaflet/leaflet-src.esm.js",
|
|
152
|
-
"leaflet/images/marker-icon.png",
|
|
153
|
-
"leaflet/images/marker-icon-2x.png",
|
|
154
|
-
"leaflet/images/marker-shadow.png",
|
|
155
|
-
|
|
156
|
-
"pica/pica.min.js",
|
|
157
|
-
"minidenticons/minidenticons.min.js",
|
|
158
|
-
"s2js/s2js.esm.js",
|
|
159
|
-
"bigfloat/esm/index.js",
|
|
160
|
-
"bigfloat/esm/BigFloat32",
|
|
161
|
-
"bigfloat/esm/BigFloat53",
|
|
162
|
-
"bigfloat/esm/BigComplex",
|
|
163
|
-
"bigfloat/esm/BaseInfo32",
|
|
164
|
-
"bigfloat/esm/util",
|
|
165
|
-
|
|
166
|
-
// TODO: the libraries
|
|
167
|
-
].map(name => new Request(name, {cache: 'no-store'}))); // Might not be necessary, but if any browsers insist on their own caching...
|
|
169
|
+
// for (const file of cacheList) cache.add(new Request(file, {cache: 'no-store'})).catch(error => console.error(file + ' ' + error.message)); // dev testing for changed file tree
|
|
170
|
+
await cache.addAll(cacheList.map(name => new Request(name, {cache: 'no-store'}))); // Might not be necessary to specify no-store, but if any browsers insist on their own caching...
|
|
168
171
|
|
|
169
172
|
await Promise.all([
|
|
170
173
|
// These are referenced within material web, but missing. Turns out we don't need them,
|
package/server/app.js
CHANGED
|
@@ -46,6 +46,12 @@ const argv = yargs(hideBin(process.argv))
|
|
|
46
46
|
default: 5,
|
|
47
47
|
description: "Additional variable seconds (+/- variableSpacing/2) to add to fixedSpacing between each portal."
|
|
48
48
|
})
|
|
49
|
+
.option('info', {
|
|
50
|
+
alias: 'i',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: true,
|
|
53
|
+
description: "Run with info logging."
|
|
54
|
+
})
|
|
49
55
|
.option('verbose', {
|
|
50
56
|
alias: 'v',
|
|
51
57
|
type: 'boolean',
|
|
@@ -94,20 +100,28 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
94
100
|
}));
|
|
95
101
|
|
|
96
102
|
app.listen(port);
|
|
97
|
-
console.log(
|
|
103
|
+
console.log(new Date(), `Listening on ${port} and starting ${argv.nPortals} nodes on ${logicalCores} ${cpus()[0].model} logical cores.`);
|
|
98
104
|
for (let i = 0; i < argv.nPortals; i++) {
|
|
99
105
|
cluster.fork();
|
|
100
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, 2e3)); // Number chosen to give an unspikey rise in packets/s.
|
|
101
107
|
}
|
|
102
108
|
} else {
|
|
103
109
|
process.title = 'axona-starting';
|
|
104
110
|
const { P2PWebNetwork, location } = await import('../index.js');
|
|
105
|
-
const network = await P2PWebNetwork.create({
|
|
111
|
+
const network = await P2PWebNetwork.create({
|
|
112
|
+
region: location,
|
|
113
|
+
infoLogger: (...rest) => argv.info && console.log(new Date(), ...rest),
|
|
114
|
+
debugLogger: (...rest) => argv.verbose && console.log(new Date(), ...rest)
|
|
115
|
+
});
|
|
106
116
|
process.title = 'axona-' + network.nodeIdentity.id;
|
|
107
|
-
|
|
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);
|
|
108
122
|
process.on('SIGINT', async () => { // Leave the network politely.
|
|
109
123
|
console.log(process.title, 'Shutdown for Ctrl+C');
|
|
110
|
-
|
|
124
|
+
clearInterval(update)
|
|
111
125
|
await network.disconnect();
|
|
112
126
|
process.exit(0);
|
|
113
127
|
});
|
package/spec/conversationSpec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { describe, it, expect, beforeAll, afterAll, BigInt } = globalThis;
|
|
1
|
+
const { describe, it, expect, expectAsync, beforeAll, afterAll, BigInt } = globalThis;
|
|
2
2
|
import { Conversation } from '../public/javascripts/conversation.js';
|
|
3
3
|
|
|
4
4
|
describe("Conversation", function () {
|
|
@@ -6,23 +6,26 @@ describe("Conversation", function () {
|
|
|
6
6
|
let conversation;
|
|
7
7
|
const tag = '123';
|
|
8
8
|
const payload = 'cake';
|
|
9
|
-
beforeAll(function () {
|
|
9
|
+
beforeAll(async function () {
|
|
10
10
|
agent = {handle: 'alice'};
|
|
11
|
-
conversation = Conversation.ensure({tag, payload, agent});
|
|
11
|
+
conversation = await Conversation.ensure({tag, payload, agent});
|
|
12
12
|
});
|
|
13
13
|
describe("creation", function () {
|
|
14
14
|
it("initializes properties.", function () {
|
|
15
15
|
expect(conversation.agent).toBe(agent);
|
|
16
16
|
expect(conversation.payload).toBe(payload);
|
|
17
17
|
});
|
|
18
|
-
it("remembers conversations with the same tag.", function () {
|
|
19
|
-
expect(Conversation.ensure({tag, payload, agent})).toBe(conversation);
|
|
18
|
+
it("remembers conversations with the same tag.", async function () {
|
|
19
|
+
expect(await Conversation.ensure({tag, payload, agent})).toBe(conversation);
|
|
20
20
|
});
|
|
21
|
-
it("Properties can be ommitted for existing tag.", function () {
|
|
22
|
-
expect(Conversation.ensure({tag, payload})).toBe(conversation);
|
|
21
|
+
it("Properties can be ommitted for existing tag.", async function () {
|
|
22
|
+
expect(await Conversation.ensure({tag, payload})).toBe(conversation);
|
|
23
23
|
});
|
|
24
|
-
it("rejects changes by default.", function () {
|
|
25
|
-
|
|
24
|
+
it("rejects changes by default.", async function () {
|
|
25
|
+
await expectAsync(Conversation.ensure({tag, payload: 'other', agent})).toBeRejected();
|
|
26
|
+
});
|
|
27
|
+
it("knows container is constructor.", function () {
|
|
28
|
+
expect(conversation.container).toBe(Conversation);
|
|
26
29
|
});
|
|
27
30
|
describe("caching", function () {
|
|
28
31
|
let keep = true;
|
|
@@ -31,68 +34,68 @@ describe("Conversation", function () {
|
|
|
31
34
|
update() { return keep && this; }
|
|
32
35
|
initialize() { return keep && this; }
|
|
33
36
|
}
|
|
34
|
-
it("ends with explicit removal.", function () {
|
|
35
|
-
let initial = Conversation.ensure({tag, agent, payload});
|
|
37
|
+
it("ends with explicit removal.", async function () {
|
|
38
|
+
let initial = await Conversation.ensure({tag, agent, payload});
|
|
36
39
|
expect(initial).toBeTruthy();
|
|
37
40
|
expect(Conversation.getItem(tag)).toBe(initial);
|
|
38
41
|
expect(Conversation.removeItem(tag)).toBe(initial);
|
|
39
42
|
expect(Conversation.getItem(tag)).toBeFalsy();
|
|
40
43
|
});
|
|
41
44
|
describe("deleting data", function () {
|
|
42
|
-
it("keeps new if not deleting.", function () {
|
|
45
|
+
it("keeps new if not deleting.", async function () {
|
|
43
46
|
tag = 'keep';
|
|
44
|
-
let initial = Conversation.ensure({tag, agent, payload});
|
|
47
|
+
let initial = await Conversation.ensure({tag, agent, payload});
|
|
45
48
|
expect(initial).toBeTruthy();
|
|
46
|
-
expect(Conversation.ensure({tag, agent, payload})).toBe(initial);
|
|
49
|
+
expect(await Conversation.ensure({tag, agent, payload})).toBe(initial);
|
|
47
50
|
expect(Conversation.getItem(tag)).toBe(initial);
|
|
48
51
|
});
|
|
49
|
-
it("is skipped if delete data.", function () {
|
|
52
|
+
it("is skipped if delete data.", async function () {
|
|
50
53
|
tag = 'skip';
|
|
51
|
-
let initial = Conversation.ensure({tag, agent});
|
|
54
|
+
let initial = await Conversation.ensure({tag, agent});
|
|
52
55
|
expect(initial).toBeFalsy();
|
|
53
56
|
expect(Conversation.getItem(tag)).toBeFalsy();
|
|
54
57
|
});
|
|
55
|
-
it("is removed if delete data.", function () {
|
|
58
|
+
it("is removed if delete data.", async function () {
|
|
56
59
|
tag = 'skip';
|
|
57
|
-
let initial = Conversation.ensure({tag, agent, payload});
|
|
60
|
+
let initial = await Conversation.ensure({tag, agent, payload});
|
|
58
61
|
expect(initial).toBeTruthy();
|
|
59
62
|
expect(Conversation.getItem(tag)).toBeTruthy();
|
|
60
|
-
Conversation.ensure({tag, agent});
|
|
63
|
+
await Conversation.ensure({tag, agent});
|
|
61
64
|
expect(Conversation.getItem(tag)).toBeFalsy();
|
|
62
65
|
});
|
|
63
66
|
});
|
|
64
67
|
describe("null/item caching convention", function () {
|
|
65
68
|
// Do we really need/want this convetion?
|
|
66
|
-
it("keeps if initialize answers conversation.", function () {
|
|
69
|
+
it("keeps if initialize answers conversation.", async function () {
|
|
67
70
|
keep = true;
|
|
68
71
|
tag = 'keepA';
|
|
69
|
-
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
72
|
+
let initial = await CacheConversation.ensure({tag, agent, payload});
|
|
70
73
|
expect(initial).toBeTruthy();
|
|
71
|
-
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
74
|
+
expect(await CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
72
75
|
expect(CacheConversation.getItem(tag)).toBe(initial);
|
|
73
76
|
});
|
|
74
|
-
it("is skipped if initialize answers falsy.", function () {
|
|
77
|
+
it("is skipped if initialize answers falsy.", async function () {
|
|
75
78
|
keep = false;
|
|
76
79
|
tag = 'skipA';
|
|
77
|
-
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
80
|
+
let initial = await CacheConversation.ensure({tag, agent, payload});
|
|
78
81
|
expect(initial).toBeFalsy();
|
|
79
82
|
expect(CacheConversation.getItem(tag)).toBeFalsy();
|
|
80
83
|
});
|
|
81
|
-
it("keeps existing if update answers conversation.", function () {
|
|
84
|
+
it("keeps existing if update answers conversation.", async function () {
|
|
82
85
|
keep = true;
|
|
83
86
|
tag = 'keepB';
|
|
84
|
-
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
85
|
-
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
86
|
-
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
87
|
-
expect(CacheConversation.getItem(tag)).toBe(initial);
|
|
87
|
+
let initial = await CacheConversation.ensure({tag, agent, payload});
|
|
88
|
+
expect(await CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
89
|
+
expect(await CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
90
|
+
expect(await CacheConversation.getItem(tag)).toBe(initial);
|
|
88
91
|
});
|
|
89
|
-
it("destroys existing if update answers falsy.", function () {
|
|
92
|
+
it("destroys existing if update answers falsy.", async function () {
|
|
90
93
|
keep = true;
|
|
91
94
|
tag = 'skipB';
|
|
92
|
-
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
93
|
-
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
95
|
+
let initial = await CacheConversation.ensure({tag, agent, payload});
|
|
96
|
+
expect(await CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
94
97
|
keep = false;
|
|
95
|
-
expect(CacheConversation.ensure({tag, agent, payload})).toBeFalsy();
|
|
98
|
+
expect(await CacheConversation.ensure({tag, agent, payload})).toBeFalsy();
|
|
96
99
|
expect(CacheConversation.getItem(tag)).toBeFalsy();
|
|
97
100
|
});
|
|
98
101
|
});
|
|
@@ -100,11 +103,11 @@ describe("Conversation", function () {
|
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
describe("replies", function () {
|
|
103
|
-
beforeAll(function () {
|
|
104
|
-
conversation.ensure({payload: "second", issuedTime: 3, tag: 'z'});
|
|
105
|
-
conversation.ensure({payload: "deleted", issuedTime: 2, tag: 'y'});
|
|
106
|
-
conversation.ensure({payload: "first", issuedTime: 1, tag: 'x'});
|
|
107
|
-
conversation.ensure({payload: null, issuedTime: 4, tag: 'y'});
|
|
106
|
+
beforeAll(async function () {
|
|
107
|
+
await conversation.ensure({payload: "second", issuedTime: 3, tag: 'z'});
|
|
108
|
+
await conversation.ensure({payload: "deleted", issuedTime: 2, tag: 'y'});
|
|
109
|
+
await conversation.ensure({payload: "first", issuedTime: 1, tag: 'x'});
|
|
110
|
+
await conversation.ensure({payload: null, issuedTime: 4, tag: 'y'});
|
|
108
111
|
});
|
|
109
112
|
it("adds replies in timestamp order.", function () {
|
|
110
113
|
expect(conversation.items.map(reply => reply.payload)).toEqual(["first", "second"]);
|
|
@@ -112,5 +115,8 @@ describe("Conversation", function () {
|
|
|
112
115
|
it("removes deleted replies.", function () { // fixme eachReply
|
|
113
116
|
expect(conversation.items.find(reply => !reply.payload)).toBeFalsy();
|
|
114
117
|
});
|
|
118
|
+
it("knows container is conversation.", function () {
|
|
119
|
+
expect(conversation.items[0].container).toBe(conversation);
|
|
120
|
+
});
|
|
115
121
|
});
|
|
116
122
|
});
|