@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 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
@@ -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.4.1",
4
+ "version": "4.4.4",
5
5
  "keywords": [
6
6
  "map",
7
7
  "browser",
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
- "@axona/protocol/std": "./axona-protocol/std/index.js",
33
- "@axona/protocol/transport/web/index.js": "./axona-protocol/src/transport/web/index.js",
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 { AxonaPeer, AxonaDomain, NeuronNode, createNodeIdentity, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION } from '@axona/protocol';
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 { webTransport } from '@axona/protocol/transport/web/index.js';
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, identity, bridgeUrl = 'wss://bridge.axona.net',
24
- synapseCount = 4, timeoutMs = 10e3} = {}) {
23
+ region = this.sessionRegion, bridgeUrl = 'wss://bridge.axona.net',
24
+ } = {}) {
25
25
  // Promise a ready-to-use network peer.
26
- // Complex region/identity behavior: Must pass either identity or region (either can be a promise), or will wait for setSessionRegion() to be called.
27
- if (!identity) region ||= this.canonicalizeRegion(await (region || this.sessionRegion));
28
- identity ||= createNodeIdentity(region);
29
- identity = await identity;
30
- region ||= identity.region;
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('Created network node for kernel', this.kernelVersion);
42
- await network.connect({synapseCount, timeoutMs});
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.leave();
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
- return data.topic;
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
- //console.log('fired', {msgId, topic, ts, signerPubkey, deleted, message});
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
- //console.log({topic, subject, payload, issuedTime, rest, signWith});
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 geoCellCenter(geoCellId(lat, lng));
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
- // TODO: Integrate with AxonaPeer's complex logging.
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
- clickTip(downloadButton, installText, () => installUpdate(serviceVersion));
95
- clickTip(downloadButton2, installText, () => event => {
96
- event.stopPropagation();
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: "Alternatively, you can update later through the button in About.", es: "Alternativamente, puede actualizar más tarde a través del botón en «Acerca de»."},
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"},
@@ -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.1';
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('../public/javascripts/p2pWebNetwork.js');
102
+ const { P2PWebNetwork, location } = await import('../index.js');
103
103
  await P2PWebNetwork.delay(cluster.worker?.id * 1e3); // One second between startups.
104
- const { location:region } = await import('./getLocation.js'); // First invocation caches.
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.
@@ -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);
@@ -1,12 +1,7 @@
1
1
  {
2
- "ip": "99.122.55.96",
3
- "hostname": "99-122-55-96.lightspeed.sntcca.sbcglobal.net",
4
- "city": "Redwood City",
5
- "region": "California",
6
- "country": "US",
7
- "loc": "37.4852,-122.2364",
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 { AxonaPeer, AxonaDomain, NeuronNode, createNodeIdentity, createAuthorIdentity, regionCenter, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, deriveTopicId, metricTopic } from '@axona/protocol';
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.transportIdentity.id.slice(0, 10), ...rest);
31
+ console.log(new Date(), this.label, this.nodeIdentity.id.slice(0, 10), ...rest);
34
32
  }
35
- static async create({location, transportIdentity, bridgeUrl = 'wss://testnet.axona.net',
36
- label = 'network', store, authorIdentity,
37
- synapseCount = 4, timeoutMs = 10e3, ...rest} = {}) {
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
- // FIXME: sometimes fails with "UpgradeRequiredError: bridge closed socket before handshake completed"
53
- const transport = webTransport({bridgeUrl, identity: transportIdentity});
54
- const node = new NeuronNode({lat: location.lat, lng: location.lng, id: BigInt('0x' + transportIdentity.id)});
55
- node.transport = transport; // TODO: pass in to constructor?
56
- const domain = new AxonaDomain({ k: 20 }); // TODO: can't this be defaulted in AxonaPeer?
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, {transportIdentity, transport, node, peer, label, authorIdentity, ...rest});
61
- self.log(`created with kernel version ${this.version} in ${(Date.now() - start).toLocaleString()} ms.`);
62
- await self.connect({synapseCount, timeoutMs});
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.peer.leave();
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
  }