@yz-social/civildefense.io 4.4.1 → 4.4.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/index.js +2 -0
- package/package.json +1 -1
- package/public/index.html +2 -5
- package/public/javascripts/agent.js +1 -1
- package/public/javascripts/main.js +2 -0
- package/public/javascripts/map.js +2 -2
- package/public/javascripts/p2pWebNetwork.js +37 -58
- package/public/javascripts/service-manager.js +7 -6
- package/public/javascripts/translations.js +1 -1
- package/public/service-worker.js +1 -1
- package/server/app.js +2 -3
- package/server/getLocation.js +2 -1
- package/server/location.json +6 -11
- package/spec/axonSpec.js +18 -57
package/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { P2PWebNetwork } from "./public/javascripts/p2pWebNetwork.js";
|
|
2
2
|
export { agentTopic, alertTopic, canonicalTag } from "./public/javascripts/versions.js";
|
|
3
|
+
export { getContainingCells } from "./public/javascripts/s2.js";
|
|
4
|
+
export { location } from "./server/getLocation.js";
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -29,11 +29,8 @@
|
|
|
29
29
|
"leaflet": "./leaflet/leaflet-src.esm.js",
|
|
30
30
|
"minidenticons": "./minidenticons/minidenticons.min.js",
|
|
31
31
|
"@axona/protocol": "./axona-protocol/src/index.js",
|
|
32
|
-
|
|
33
|
-
"@axona/protocol/
|
|
34
|
-
"@yz-social/kdht": "./kdht/index.js",
|
|
35
|
-
"@yz-social/webrtc": "./webrtc/index.js",
|
|
36
|
-
"scripting": "./javascripts/scripting.js"
|
|
32
|
+
"@axona/protocol/std": "./axona-protocol/std/index.js",
|
|
33
|
+
"@axona/protocol/connect.js": "./axona-protocol/src/connect.js"
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
</script>
|
|
@@ -136,7 +136,7 @@ export class Agent {
|
|
|
136
136
|
// Our downsampling is such that we do not need to chunkify.
|
|
137
137
|
// if (type === 'avatar') {
|
|
138
138
|
// const blob = await P2PWebNetwork.dataURL2blob(value);
|
|
139
|
-
// payload = await contact.chunkifyBlob({blob, region});
|
|
139
|
+
// payload = (await contact.chunkifyBlob({blob, region})).topic;
|
|
140
140
|
// }
|
|
141
141
|
return contact.publish({eventName, region, owner, payload});
|
|
142
142
|
}
|
|
@@ -41,6 +41,7 @@ export function tooltip(elementOrQuerySelector, text) { // Add plain tooltip tex
|
|
|
41
41
|
}
|
|
42
42
|
export function clickTip(elementOrQuerySelector, text, clickHandler) { // tooltip and onclick in one step.
|
|
43
43
|
const element = asElement(elementOrQuerySelector);
|
|
44
|
+
if (element.id === 'downloadUpdates2') console.log('assigning title', text, 'and', clickHandler, 'to', element);
|
|
44
45
|
tooltip(element, text);
|
|
45
46
|
element.onclick = clickHandler;
|
|
46
47
|
}
|
|
@@ -363,6 +364,7 @@ document.querySelector('head > title').innerHTML = titleElement.textContent;
|
|
|
363
364
|
function initText(selector, content = selector) {
|
|
364
365
|
const element = document.querySelector(selector);
|
|
365
366
|
const text = Int([content]);
|
|
367
|
+
if (element.id === 'downloadUpdates2') console.log('assigning', text, 'to', element);
|
|
366
368
|
element.textContent = text;
|
|
367
369
|
}
|
|
368
370
|
initText('#aboutReport');
|
|
@@ -486,7 +486,7 @@ export class Marker { // A wrapper around L.marker
|
|
|
486
486
|
inputElement.querySelector('md-filled-icon-button').toggleAttribute('disabled', true);
|
|
487
487
|
const contact = await networkPromise;
|
|
488
488
|
if (files.length) {
|
|
489
|
-
const file = await contact.chunkifyBlob({blob: files[0], region});
|
|
489
|
+
const {topic:file} = await contact.chunkifyBlob({blob: files[0], region});
|
|
490
490
|
payload = {message: payload, file};
|
|
491
491
|
}
|
|
492
492
|
await contact.publish({eventName: subject, region, payload}); // Publish the new reply.
|
|
@@ -502,7 +502,7 @@ export class Marker { // A wrapper around L.marker
|
|
|
502
502
|
const formatReply = ({subject, payload, ...rest}) => {
|
|
503
503
|
const {message = payload, file, name} = payload;
|
|
504
504
|
let text = message
|
|
505
|
-
.replace(/https?:\/\/\S+\.(mp3|aac|ogg|oga|opus|m4a|m3u8|mpd)$/ig, url => `<audio controls src="${url}"></audio>`) // show audio urls as players
|
|
505
|
+
.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
|
|
506
506
|
.replace(/https?:\/\/\S+\.(mp4|mov|webm)$/ig, url => `<video controls src="${url}"></video>`) // show video urls as players
|
|
507
507
|
.replace(/(?<!")https?:\/\/\S+/g, url => `<a href="${url}" target="yz.sidebar">${url}</a>`); // show urls as links
|
|
508
508
|
let attachment = '';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import {
|
|
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
|
-
import {
|
|
4
|
+
import { connect } from '@axona/protocol/connect.js';
|
|
5
5
|
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
|
|
6
6
|
const { BigInt, URL, File, pica } = globalThis;
|
|
7
7
|
|
|
@@ -20,60 +20,36 @@ export class P2PWebNetwork {
|
|
|
20
20
|
static setSessionRegion = resolveSessionRegion;
|
|
21
21
|
static sessionRegion = sessionRegionPromise;
|
|
22
22
|
static async create({infoLogger = console.log, debugLogger,
|
|
23
|
-
region
|
|
24
|
-
|
|
23
|
+
region = this.sessionRegion, bridgeUrl = 'wss://bridge.axona.net',
|
|
24
|
+
} = {}) {
|
|
25
25
|
// Promise a ready-to-use network peer.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const transport = webTransport({bridgeUrl, identity});
|
|
33
|
-
const node = new NeuronNode({lat: region.lat, lng: region.lng, id: BigInt('0x' + identity.id)});
|
|
34
|
-
node.transport = transport; // FIXME: pass in to constructor?
|
|
35
|
-
const domain = new AxonaDomain({ k: 20 }); // FIXME: can't this be defaulted in AxonaPeer?
|
|
36
|
-
const peer = new AxonaPeer({domain, node, identity, transport});
|
|
26
|
+
const { peer, nodeIdentity, status, disconnect } = await connect({
|
|
27
|
+
bridge: bridgeUrl,
|
|
28
|
+
location: await region,
|
|
29
|
+
author: false
|
|
30
|
+
});
|
|
37
31
|
|
|
38
32
|
const network = new this();
|
|
39
|
-
Object.assign(network, {infoLogger, debugLogger, identity, transport, node, peer});
|
|
33
|
+
Object.assign(network, {infoLogger, debugLogger, /*identity, transport, node,*/ disconnector: disconnect, identity: nodeIdentity, peer});
|
|
40
34
|
network.resetStatePromises();
|
|
41
|
-
network.info(
|
|
42
|
-
|
|
35
|
+
network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(region.lat, region.lng).toString(16)}.`);
|
|
36
|
+
const { peers, ms } = status;
|
|
37
|
+
network.info(`Connected ${peers} connections in ${ms.toLocaleString()} ms.`);
|
|
38
|
+
network.attached(network);
|
|
43
39
|
return network;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
async connect({synapseCount = 4, timeoutMs = 10e3} = {}) {
|
|
47
|
-
// Returned promise resolves when ready for use. Can be cycled through disconnect()/connect().
|
|
48
|
-
await this.transport.start(this.identity.id);
|
|
49
|
-
await this.join();
|
|
50
|
-
this.debug('Joined', this.health().synaptomeSize, 'connections.');
|
|
51
|
-
if (parseInt(this.constructor.kernelVersion) < 4) {
|
|
52
|
-
const t0 = Date.now();
|
|
53
|
-
while (Date.now() - t0 < timeoutMs) {
|
|
54
|
-
const size = this.synaptomeSize;
|
|
55
|
-
if (size >= synapseCount) break;
|
|
56
|
-
await this.constructor.delay(200);
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
59
|
-
await this.peer.ready({ minPeers: synapseCount, timeoutMs });
|
|
60
|
-
}
|
|
61
|
-
this.info('Connected', this.health().synaptomeSize, 'connections.');
|
|
62
|
-
this.attached(this);
|
|
63
|
-
return this;
|
|
64
|
-
}
|
|
65
42
|
async disconnect() { // Politely close network connection.
|
|
66
|
-
const health = this.health();
|
|
67
|
-
await this.
|
|
43
|
+
const health = this.peer.health();
|
|
44
|
+
await this.disconnector();
|
|
68
45
|
this.info(`disconnected with ${health.peers.length} connections and ${health.axonRoles.length} axons.`);
|
|
69
|
-
await this.stop();
|
|
70
46
|
this.resetStatePromises();
|
|
71
47
|
}
|
|
72
48
|
async replicateStorage() { // Let the network know that we might go away without further notice.
|
|
73
49
|
// FIXME. It would be great if we could remove ourselves from any non-leaf positions in the Axon, but stay subscribed.
|
|
74
50
|
}
|
|
75
51
|
fastDisconnect() { // Synchronous attempt to be polite to those connected.
|
|
76
|
-
this.leave(); // Execution is asynchronous. Will not finish -- or perhaps even really start -- within the call.
|
|
52
|
+
this.peer.leave(); // Execution is asynchronous. Will not finish -- or perhaps even really start -- within the call.
|
|
77
53
|
}
|
|
78
54
|
|
|
79
55
|
async chunkifyString({string, region, signWith = this.constructor.currentPublishIdentity, owner = signWith.authorId}) {
|
|
@@ -88,7 +64,7 @@ export class P2PWebNetwork {
|
|
|
88
64
|
return bytesToString(data.bytes);
|
|
89
65
|
}
|
|
90
66
|
|
|
91
|
-
static getCanvas(file) { // Promise a Canvas from a File of type image/*.
|
|
67
|
+
static getCanvas(file) { // Promise a Canvas from a File of type image/*. ONLY IN BROWSERS!
|
|
92
68
|
return new Promise((resolve, reject) => {
|
|
93
69
|
const img = new Image();
|
|
94
70
|
const canvas = document.createElement('canvas');
|
|
@@ -104,7 +80,7 @@ export class P2PWebNetwork {
|
|
|
104
80
|
img.src = URL.createObjectURL(file);
|
|
105
81
|
});
|
|
106
82
|
}
|
|
107
|
-
static async downsampledBlob({blob, outputType = 'image/jpeg', maxDimension = 1024}) {
|
|
83
|
+
static async downsampledBlob({blob, outputType = 'image/jpeg', maxDimension = 1024}) { // ONLY IN BROWSERS!
|
|
108
84
|
// Promise a reasonably sized Blob (or File) for a given Blob of type image/*, else blob unchanged.
|
|
109
85
|
if (!blob.type.startsWith('image/')) return blob;
|
|
110
86
|
|
|
@@ -152,22 +128,24 @@ export class P2PWebNetwork {
|
|
|
152
128
|
return new File([blob], filename, {type: blob.type});
|
|
153
129
|
}
|
|
154
130
|
async chunkifyBlob({blob, region, signWith = this.constructor.currentPublishIdentity, owner = signWith.authorId, maxDimension = 1024, ...rest}) {
|
|
155
|
-
// Publish Blob (or File) and answer an identifier that can be used to re-assemble.
|
|
131
|
+
// Publish Blob (or File) and answer an identifier that can be used to re-assemble. Truthy maxDimension works only in browsers!
|
|
156
132
|
if (!blob.size) throw new Error(`Cannot chunkify empty Blob.`);
|
|
157
133
|
if (maxDimension) blob = await this.constructor.downsampledBlob({blob, maxDimension});
|
|
158
134
|
const {type:mime, name} = blob;
|
|
159
135
|
const topic = {name: uuidv4(), region, owner};
|
|
160
136
|
const buffer = await blob.arrayBuffer();
|
|
161
137
|
const u8 = new Uint8Array(buffer);
|
|
162
|
-
console.log('blob', blob.size, u8.length);
|
|
138
|
+
//console.log('blob', blob.size, u8.length);
|
|
163
139
|
const data = await publishChunkedBytes(this.peer, u8, {topic, signWith, mime, name, ...rest});
|
|
164
|
-
|
|
140
|
+
this.debug('chunked', data);
|
|
141
|
+
return data;
|
|
165
142
|
}
|
|
166
143
|
async assembleChunkedDataURL(topic) { // Promise {bytes, mime, name, dataURL} that was chunkified to topic.
|
|
167
144
|
const data = await receiveChunkedBytes(this.peer, topic, {/*, onProgress: console.log*/});
|
|
168
145
|
// Using dataURL is not terribly efficient, but it is convenient, because formatReplies can return HTML strings with all the data in them,
|
|
169
146
|
// instead of, e.g., needing javascript to later set properties of elements to createObjectURL of a Blob.
|
|
170
147
|
data.dataURL = this.constructor.u82dataURL(data.bytes, data.mime);
|
|
148
|
+
this.debug('assembled', topic);
|
|
171
149
|
return data;
|
|
172
150
|
}
|
|
173
151
|
|
|
@@ -181,7 +159,7 @@ export class P2PWebNetwork {
|
|
|
181
159
|
if (handler) {
|
|
182
160
|
const callback = async envelope => {
|
|
183
161
|
const {message, deleted, msgId, signerPubkey, topic, ts} = envelope;
|
|
184
|
-
|
|
162
|
+
this.debug('received', {msgId, topic, ts, signerPubkey, deleted, message});
|
|
185
163
|
if (deleted) {
|
|
186
164
|
handler({subject: msgId, payload: null, agent: signerPubkey, topic, ts}); // fixme remove topic, ts here and below.
|
|
187
165
|
return;
|
|
@@ -200,17 +178,26 @@ export class P2PWebNetwork {
|
|
|
200
178
|
const topic = {region, name: eventName};
|
|
201
179
|
if (owner) topic.owner = owner;
|
|
202
180
|
const options = {signWith};
|
|
203
|
-
|
|
181
|
+
this.debug('published', {topic, subject, payload, issuedTime, rest, signWith});
|
|
204
182
|
if (payload) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
|
|
205
183
|
// The next would not normally happen, but until since:'latest' works, we need a way to send a null payload and have the handler delete the entry.
|
|
206
184
|
if (!subject) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
|
|
207
185
|
return await this.peer.kill(topic, subject, options);
|
|
208
186
|
}
|
|
209
187
|
|
|
188
|
+
host() {
|
|
189
|
+
return this.peer.host();
|
|
190
|
+
}
|
|
191
|
+
unhost() {
|
|
192
|
+
return this.peer.unhost();
|
|
193
|
+
}
|
|
210
194
|
// Mostly internal stuff.
|
|
211
195
|
static regionCode(lat, lng) { // Answer containing region code.
|
|
212
196
|
return geoCellId(lat, lng);
|
|
213
197
|
}
|
|
198
|
+
static regionCenter(regionCode) {
|
|
199
|
+
return geoCellCenter(regionCode);
|
|
200
|
+
}
|
|
214
201
|
static delay(ms, result) { // Promise result after ms milliseconds.
|
|
215
202
|
return new Promise(resolve => setTimeout(resolve, ms, result));
|
|
216
203
|
}
|
|
@@ -224,12 +211,9 @@ export class P2PWebNetwork {
|
|
|
224
211
|
static canonicalizeRegion(lat, lng) {
|
|
225
212
|
// Answer a {lat, lng} that is the center of a top-level Axona region containing the given {lat, lng}.
|
|
226
213
|
// E.g., a precise location gets anonymized to containing top-level cell center.
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
get synaptomeSize() { // Safely answer the number of connections.
|
|
230
|
-
return this.node.synaptome?.size ?? 0;
|
|
214
|
+
return this.regionCenter(this.regionCode(lat, lng));
|
|
231
215
|
}
|
|
232
|
-
//
|
|
216
|
+
// Todo: Integrate with AxonaPeer's complex logging.
|
|
233
217
|
debug(...rest) { // Add debug logspam.
|
|
234
218
|
this.debugLogger?.(this.identity.id, ...rest);
|
|
235
219
|
}
|
|
@@ -238,8 +222,3 @@ export class P2PWebNetwork {
|
|
|
238
222
|
}
|
|
239
223
|
}
|
|
240
224
|
export default P2PWebNetwork;
|
|
241
|
-
|
|
242
|
-
// For now, we want to override publish and subscribe, but that conflicts with internal messages on AxonaPeer.
|
|
243
|
-
// Thus P2PWebNetwork has an AxonaPeer, instead of inheriting from it. And thus we need forwarding messages.
|
|
244
|
-
['join', 'leave', 'stop', 'health', 'host', 'unhost']
|
|
245
|
-
.forEach(methodName => P2PWebNetwork.prototype[methodName] = function (...rest) {return this.peer[methodName](...rest);});
|
|
@@ -71,7 +71,10 @@ function newVersionAvailable(newVersion) {
|
|
|
71
71
|
updateText.textContent = `${Int`Version`} ${newVersion} ${Int`available`}.`;
|
|
72
72
|
openDisplay('updateContainer');
|
|
73
73
|
}
|
|
74
|
-
async function installUpdate(newVersion) {
|
|
74
|
+
async function installUpdate(event, newVersion) {
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
event.target.textContent = "Installing..."; // In case there is some delay, tell the user what we're trying to do. Will be cleared with reload.
|
|
77
|
+
event.target.disabled = true;
|
|
75
78
|
await caches.delete(appVersion); // Must be before cacheSource, or we'll just recache the same files!
|
|
76
79
|
await cacheSource(newVersion);
|
|
77
80
|
// Reload, but convince all browsers to re-"fetch" (through the new service worker that is now running).
|
|
@@ -91,11 +94,9 @@ await navigator.serviceWorker
|
|
|
91
94
|
let serviceVersion;
|
|
92
95
|
// No need to reset button/status on click, because we will be reloading.
|
|
93
96
|
const installText = Int`Update to a new version of this app.`;
|
|
94
|
-
|
|
95
|
-
clickTip(
|
|
96
|
-
|
|
97
|
-
installUpdate(serviceVersion);
|
|
98
|
-
});
|
|
97
|
+
const installHandler = event => installUpdate(event, serviceVersion);
|
|
98
|
+
clickTip(downloadButton, installText, installHandler);
|
|
99
|
+
clickTip(downloadButton2, installText, installHandler);
|
|
99
100
|
clickTip(checkButton, Int`Check to see if a new version of the app is available.`, async event => {
|
|
100
101
|
resetInactivityTimer();
|
|
101
102
|
event.stopPropagation();
|
|
@@ -71,7 +71,7 @@ const translations = {
|
|
|
71
71
|
['#newVersionHeader']: {en: "New version available", es: "Nueva versión disponible"},
|
|
72
72
|
['#updateNowQuestion']: {en: "Would you like to update now?", es: "¿Le gustaría actualizar ahora?"},
|
|
73
73
|
['#updateReload']: {en: "All CivilDefense.io tabs will reload.", es: "Todas las pestañas de CivilDefense.io se recargarán."},
|
|
74
|
-
['#updateDefer']: {en:
|
|
74
|
+
['#updateDefer']: {en: 'Alternatively, you can update later through the "CD" button again.', es: 'Alternativamente, puede actualizar más tarde utilizando nuevamente el botón "CD".'},
|
|
75
75
|
['#downloadUpdates2']: {en: "yes, update", es: "Sí, actualizar."},
|
|
76
76
|
['#downloadDefer']: {en: "no, not yet", es: "No, todavía no."},
|
|
77
77
|
['No update at']: {es: "No hay actualizaciones a las"},
|
package/public/service-worker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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.4.
|
|
3
|
+
const serviceVersion = '4.4.4';
|
|
4
4
|
|
|
5
5
|
async function cacheFirst({request, event}) {
|
|
6
6
|
// Handle request from any cache, else fetch and store it in serviceCache.
|
package/server/app.js
CHANGED
|
@@ -99,10 +99,9 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
99
99
|
for (let i = 0; i < argv.nPortals; i++) cluster.fork();
|
|
100
100
|
} else {
|
|
101
101
|
process.title = 'axona-starting';
|
|
102
|
-
const { P2PWebNetwork } = await import('../
|
|
102
|
+
const { P2PWebNetwork, location } = await import('../index.js');
|
|
103
103
|
await P2PWebNetwork.delay(cluster.worker?.id * 1e3); // One second between startups.
|
|
104
|
-
const
|
|
105
|
-
const network = await P2PWebNetwork.create({region});
|
|
104
|
+
const network = await P2PWebNetwork.create({region: location});
|
|
106
105
|
process.title = 'axona-' + network.identity.id;
|
|
107
106
|
let update = null//setInterval(() => network.info(network.health().axonRoles.map(role => role.topic)), 5e3);
|
|
108
107
|
process.on('SIGINT', async () => { // Leave the network politely.
|
package/server/getLocation.js
CHANGED
|
@@ -6,7 +6,8 @@ import fs from 'fs/promises';
|
|
|
6
6
|
import { resolve } from './dirname.js';
|
|
7
7
|
const filename = './location.json';
|
|
8
8
|
export const data = await import(filename, {with: { type: 'json' }})
|
|
9
|
-
.catch(async
|
|
9
|
+
.catch(async error => {
|
|
10
|
+
console.log(error);
|
|
10
11
|
const response = await fetch('https://ipinfo.io/json');
|
|
11
12
|
const string = await response.text();
|
|
12
13
|
console.log('Estimating location as', string);
|
package/server/location.json
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"org": "AS7018 AT&T Enterprises, LLC",
|
|
9
|
-
"postal": "94061",
|
|
10
|
-
"timezone": "America/Los_Angeles",
|
|
11
|
-
"readme": "https://ipinfo.io/missingauth"
|
|
12
|
-
}
|
|
2
|
+
"uswest/80": "37.4852,-122.2364",
|
|
3
|
+
"useast/89": "40,-75",
|
|
4
|
+
"uscentle/88": "35,-83",
|
|
5
|
+
"easteu/47": "50,17",
|
|
6
|
+
"loc": "37.4852,-122.2364"
|
|
7
|
+
}
|
package/spec/axonSpec.js
CHANGED
|
@@ -21,76 +21,43 @@
|
|
|
21
21
|
David will join and subscribe after publications.
|
|
22
22
|
*/
|
|
23
23
|
const { describe, it, expect, beforeAll, afterAll, BigInt } = globalThis;
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
// TODO: What is the right way to use Axona web transport. It doesn't seem to provide either a functioning export nor declare its dependencies.
|
|
27
|
-
import { webTransport } from '@axona/protocol/transport/web/index.js';
|
|
24
|
+
import { createAuthorIdentity, regionCenter, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, deriveTopicId, metricTopic } from '@axona/protocol';
|
|
25
|
+
import { connect } from '@axona/protocol/connect.js';
|
|
28
26
|
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
|
|
29
27
|
|
|
30
28
|
class Node { // Stuff we have to do every time. TODO: build something like this into Axona.
|
|
31
29
|
static version = KERNEL_VERSION;
|
|
32
30
|
log(...rest) {
|
|
33
|
-
console.log(new Date(), this.label, this.
|
|
31
|
+
console.log(new Date(), this.label, this.nodeIdentity.id.slice(0, 10), ...rest);
|
|
34
32
|
}
|
|
35
|
-
static async create({location,
|
|
36
|
-
label = 'network',
|
|
37
|
-
|
|
33
|
+
static async create({location, bridgeUrl = 'wss://testnet.axona.net',
|
|
34
|
+
label = 'network', authorIdentity, store,
|
|
35
|
+
...rest} = {}) {
|
|
38
36
|
// Promise a ready-to-use network peer.
|
|
39
|
-
|
|
40
|
-
const start = Date.now();
|
|
41
|
-
// Complex location/identity behavior: Must pass either identity or location {lat/lng} (either can be a promise).
|
|
42
|
-
if (!transportIdentity) location ||= this.canonicalizeRegion(await location);
|
|
43
|
-
transportIdentity ||= createNodeIdentity(location);
|
|
44
|
-
transportIdentity = await transportIdentity;
|
|
45
|
-
location ||= transportIdentity.region; // TODO: don't use the same term 'region' in different ways.
|
|
46
|
-
|
|
47
37
|
if (typeof(authorIdentity) === 'string') // TODO: this is pretty awkward.
|
|
48
38
|
authorIdentity = createAuthorIdentity({persistAs: label, store: {get() { return authorIdentity; }}});
|
|
49
39
|
authorIdentity ||= createAuthorIdentity({persistAs: label, store});
|
|
50
40
|
authorIdentity = await authorIdentity;
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const peer = new AxonaPeer({domain, node, identity: transportIdentity, transport});
|
|
42
|
+
const { peer, nodeIdentity, status, disconnect } = await connect({
|
|
43
|
+
bridge: bridgeUrl,
|
|
44
|
+
location: await location,
|
|
45
|
+
author: authorIdentity
|
|
46
|
+
});
|
|
58
47
|
|
|
59
48
|
const self = new this();
|
|
60
|
-
Object.assign(self, {
|
|
61
|
-
self.log(`created with kernel version ${this.version}
|
|
62
|
-
|
|
49
|
+
Object.assign(self, {peer, label, authorIdentity, nodeIdentity, disconnector: disconnect, ...rest});
|
|
50
|
+
self.log(`created with kernel version ${this.version}.`);
|
|
51
|
+
const { peers, ms } = status;
|
|
52
|
+
self.log(`connected ${peers} connections in ${ms.toLocaleString()} ms.`);
|
|
53
|
+
|
|
63
54
|
return self;
|
|
64
55
|
}
|
|
65
|
-
async connect({synapseCount = 4, timeoutMs = 10e3} = {}) {
|
|
66
|
-
// Returned promise resolves when ready for use.
|
|
67
|
-
// TODO: Currently, one cannot disconnect() and then later connect() - one is likely to get TransportError: bridge socket closed before open.
|
|
68
|
-
// So as it stands there's not really any point in this being a separate method from create().
|
|
69
|
-
const start = Date.now();
|
|
70
|
-
await this.transport.start(this.transportIdentity.id);
|
|
71
|
-
await this.peer.join();
|
|
72
|
-
if (parseInt(this.constructor.version) < 4) {
|
|
73
|
-
const t0 = Date.now();
|
|
74
|
-
while (Date.now() - t0 < timeoutMs) {
|
|
75
|
-
const size = this.synaptomeSize;
|
|
76
|
-
if (size >= synapseCount) break;
|
|
77
|
-
await this.constructor.delay(200);
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
await this.peer.ready({ minPeers: synapseCount, timeoutMs });
|
|
81
|
-
}
|
|
82
|
-
const elapsed = Date.now() - start;
|
|
83
|
-
// FIXME: most of the time, this completes in under two seconds. But not infrequently, it is much more.
|
|
84
|
-
// I've see it take 30 seconds -- even when there are nearby nodes standing by.
|
|
85
|
-
this.log(`connected to ${this.synaptomeSize} nodes in ${elapsed.toLocaleString()} ms${elapsed < 2e3 ? '.' : '!!!!!!!!!!!!!!!!'}`);
|
|
86
|
-
return this;
|
|
87
|
-
}
|
|
88
56
|
async disconnect() { // Politely close network connection.
|
|
89
57
|
const start = Date.now();
|
|
90
|
-
const health = this.health();
|
|
91
|
-
await this.
|
|
58
|
+
const health = this.peer.health();
|
|
59
|
+
await this.disconnector();
|
|
92
60
|
this.log(`disconnected with ${health.peers.length} connections and ${health.axonRoles.length} axons in ${(Date.now() - start).toLocaleString()} ms.`);
|
|
93
|
-
await this.peer.stop();
|
|
94
61
|
}
|
|
95
62
|
async subscribe({eventName, region, owner, since = 'all', handler}) { // Assign handler for eventName, or remove any handler if falsy.
|
|
96
63
|
const topic = {region, name: eventName};
|
|
@@ -133,12 +100,6 @@ class Node { // Stuff we have to do every time. TODO: build something like this
|
|
|
133
100
|
static delay(ms, result) { // Promise result after ms milliseconds.
|
|
134
101
|
return new Promise(resolve => setTimeout(resolve, ms, result));
|
|
135
102
|
}
|
|
136
|
-
get synaptomeSize() { // Safely answer the number of connections.
|
|
137
|
-
return this.node.synaptome?.size ?? 0;
|
|
138
|
-
}
|
|
139
|
-
health() { // from peer
|
|
140
|
-
return this.peer.health();
|
|
141
|
-
}
|
|
142
103
|
host() { // through peer
|
|
143
104
|
return this.peer.host();
|
|
144
105
|
}
|