@yz-social/civildefense.io 4.5.21 → 4.5.23

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.
@@ -19,7 +19,7 @@ export class Tagged { // Maintains cached existence within a (possibly instance-
19
19
  for (const key in properties) {
20
20
  const existing = this[key];
21
21
  const proposed = properties[key];
22
- if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update '${key}' from ${existing} to ${proposed}.`);
22
+ if (JSON.stringify(existing) !== JSON.stringify(proposed)) throw new Error(`Cannot update '${key}' in ${this.constructor.name} ${this.tag} from ${JSON.stringify(existing)} to ${JSON.stringify(proposed)}.`);
23
23
  }
24
24
  return this;
25
25
  }
@@ -32,13 +32,15 @@ export class Tagged { // Maintains cached existence within a (possibly instance-
32
32
 
33
33
  static async ensureIn(data, container, kind = container.itemKind) { // update() or initialize() item and remember what those answer. (Falsy is deleted).
34
34
  const {tag, payload, ...rest} = data;
35
- let item = container.getItem(tag);
35
+ let item = container.getItem(tag); // existing item
36
36
  if (!payload) return item?.destroy();
37
+
38
+ // update or create item.
37
39
  if (item) item = await item.update(data);
38
40
  else item = await (new kind().initialize({...data, container}));
39
41
 
40
42
  if (!item) return container.removeItem(tag)?.destroy();
41
- return container.setItem(tag, item);
43
+ return container.setItem(item.tag, item); // initialize() may come up with a better tag.
42
44
  }
43
45
  }
44
46
 
@@ -133,7 +133,8 @@ export const Hashtags = {
133
133
  return `<md-filter-chip label="${label}" elevated removable
134
134
  ${active === 'pub' ? 'class="pub"' : ''}
135
135
  ${active ? ' selected' : ''}
136
- >${this.firstEmoji(label) ? '' : this.identicon(label, 'selected-icon')}
136
+ >${this.firstEmoji(label) ? '' :
137
+ `<div slot="selected-icon" class="identicon"><md-icon class="material-icons">checkmark</md-icon> ${this.identicon(label)}</div>`}
137
138
  <md-icon-button slot="remove-trailing-icon"><md-icon class="material-icons"></md-icon></md-icon-button>
138
139
  </md-filter-chip>`;
139
140
  },
@@ -83,7 +83,7 @@ export var trackMap;
83
83
  export function initMap(lat, lng, zoom, positionLabel) { // Set up appropriate zoomed initial map and handlers for this position.
84
84
  // Then show initial message and updateSubscriptions.
85
85
 
86
- P2PWebNetwork.setSessionRegion({lat, lng});
86
+ P2PWebNetwork.setSessionLocation({lat, lng});
87
87
 
88
88
  // Map will be centered at the given current location marker, unless overriden by query parameters.
89
89
  let center = {lat, lng};
@@ -1,5 +1,5 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
- import { connect, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION } from './protocol.js';
2
+ import { connect, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, dht } from './protocol.js';
3
3
  import { stringToBytes, bytesToString, publishChunkedBytes, receiveChunkedBytes } from './protocol.js';
4
4
 
5
5
  if (!Uint8Array.prototype.toBase64) { // NodeJS < 24
@@ -16,37 +16,38 @@ const { BigInt, URL, File, pica } = globalThis;
16
16
  await network.disconnect();
17
17
  */
18
18
 
19
- const {promise:sessionRegionPromise, resolve:resolveSessionRegion} = Promise.withResolvers();
19
+ const {promise:sessionLocationPromise, resolve:resolveSessionLocation} = Promise.withResolvers();
20
20
 
21
21
  export class P2PWebNetwork {
22
22
  static wireVersion = WIRE_VERSION;
23
23
  static kernelVersion = KERNEL_VERSION;
24
24
  static createAuthorIdentity = createAuthorIdentity;
25
- static setSessionRegion = resolveSessionRegion;
26
- static sessionRegion = sessionRegionPromise;
25
+ static setSessionLocation = resolveSessionLocation;
26
+ static sessionLocation = sessionLocationPromise;
27
27
  static async create({infoLogger = console.log, debugLogger,
28
- region = this.sessionRegion,
28
+ location = this.sessionLocation,
29
29
  bridgeUrl = (globalThis.location && new URL(globalThis.location).searchParams.get('bridge')) ||
30
30
  globalThis.process?.env.BRIDGE_URL ||
31
- (parseInt(new URL(globalThis.location || 'file://').searchParams.get('dht')) <= 0 && // fixme remove when we host bridges
32
- globalThis.location?.origin.replace(/^http/, 'ws')) ||
31
+ ((dht <= 0) && // fixme remove when we host bridges
32
+ (globalThis.location?.origin.replace(/^http/, 'ws') ||
33
+ 'ws://localhost:3000')) ||
33
34
  'wss://bridge.axona.net',
34
35
  } = {}) {
35
36
  // Promise a ready-to-use network peer.
36
- region = await region;
37
+ location = await location;
37
38
 
38
39
  const network = new this();
39
40
  network.resetStatePromises();
40
41
 
41
42
  const { peer, nodeIdentity, transport, status, disconnect } = await connect({
42
43
  bridge: bridgeUrl,
43
- location: region,
44
+ location,
44
45
  onDisconnect: network.detached,
45
46
  author: false
46
47
  });
47
48
  Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer});
48
49
 
49
- network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(region.lat, region.lng).toString(16)}.`);
50
+ network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(location.lat, location.lng).toString(16)}.`);
50
51
  peer.onError(error => {
51
52
  network.info(`error: ${error.message || error}`);
52
53
  throw error;
@@ -250,7 +251,7 @@ export class P2PWebNetwork {
250
251
  }
251
252
  shortHealth(health = this.peer.health()) { // Info-report a short form of health.
252
253
  const roots = health.axonRoles.filter(r => r.isRoot);
253
- this.info(`disconnected with connections ${health.peers.map(P2PWebNetwork.short)}\nand ${roots.length ? `roots ${roots.map(r => P2PWebNetwork.short(r.topic))}.` : 'no roots.'}`);
254
+ this.info(`disconnected with ${health.peers.length} connections: ${health.peers.map(P2PWebNetwork.short)}\nand ${roots.length ? `roots ${roots.map(r => P2PWebNetwork.short(r.topic))}.` : 'no roots.'}`);
254
255
  }
255
256
  inRegionConnections(health = this.peer.health()) {
256
257
  const region = this.nodeIdentity.id.slice(0, 2);
@@ -3,15 +3,18 @@ const { TextEncoder, TextDecoder, BigInt, URL, WebSocket, Buffer } = globalThis;
3
3
 
4
4
  let connect, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, stringToBytes, bytesToString, publishChunkedBytes, receiveChunkedBytes;
5
5
 
6
- // dht 1 -> Axona (default)
6
+ // dht 1 -> Axona
7
7
  // dht 0 -> server
8
8
  // dht -1 -> in-memory on client only
9
- const dht = parseInt(globalThis.process ? globalThis.process.env.DHT : new URL(globalThis.location).searchParams.get('dht'));
9
+ const defaultDHT = globalThis.process ? 1 : 0; // Browser defaults to no Axona. Server nodes and alert-bot default to Axona.
10
+ export const dht = parseInt((globalThis.process ?
11
+ globalThis.process.env.DHT :
12
+ new URL(globalThis.location).searchParams.get('dht')) ?? defaultDHT);
10
13
 
11
14
  if (dht < 1) {
12
15
 
13
16
  const { v4:uuidv4 } = await import('uuid');
14
- const { getContainingCells, getPointInCell } = await import('./s2.js');
17
+ const { getSmallestCellId, getPointInCell } = await import('./s2.js');
15
18
  const { cellHex } = await import('./versions.js');
16
19
  const operator = await import('./pubsub.js');
17
20
 
@@ -35,8 +38,8 @@ if (dht < 1) {
35
38
  return {authorId: tag};
36
39
  };
37
40
  geoCellId = (lat, lng) => {
38
- const cells = getContainingCells(lat, lng);
39
- const hex = cellHex(cells[0]);
41
+ const cellid = getSmallestCellId(lat, lng);
42
+ const hex = cellHex(cellid);
40
43
  const sliced = hex.slice(0, 2);
41
44
  return parseInt(sliced, 16); // The worst way to do this.
42
45
  };
@@ -93,16 +96,18 @@ if (dht < 1) {
93
96
  const [tag, ...rest] = JSON.parse(event.data);
94
97
  const subHandler = handlers[tag];
95
98
  const inFlightResolver = inFlight[tag];
96
- if ((!subHandler && !inFlightResolver) ||
99
+
100
+ if ((!subHandler && !inFlightResolver) || // debug
97
101
  ((typeof(subHandler) !== 'function') && (typeof(inFlightResolver) !== 'function')))
98
- console.log({tag, rest, subHandler, inFlightResolver, handlers, inFlight});
102
+ console.warn('no handler or request', {tag, rest, subHandler, inFlightResolver, handlers, inFlight});
103
+
99
104
  if (subHandler) return subHandler(...rest);
100
105
  delete inFlight[tag];
101
106
  return inFlightResolver?.(...rest);
102
107
  };
103
108
  socket.onopen = () => {
104
109
  if (socket.readyState !== WebSocket.OPEN) return; // You would think that can't happen, but...
105
- resolve((...rest) => { // send()
110
+ resolve((...rest) => { // Promise the send() function.
106
111
  const tag = uuidv4();
107
112
  const {promise, resolve} = Promise.withResolvers();
108
113
  inFlight[tag] = resolve;
@@ -149,10 +154,12 @@ if (dht < 1) {
149
154
  kill(topic, msgId, options) {
150
155
  return send('unpublish', topic, msgId, options);
151
156
  },
157
+ lookup() { return {}; },
152
158
  host() {},
153
159
  unhost() {}
154
160
  };
155
161
  const status = {peers: 0, ms: 0}; // fixme ms
162
+ await new Promise(resolve => setTimeout(resolve, 1e3)); // Simulate finding peers, and give the app some time to do stuff.
156
163
  return { peer, nodeIdentity, transport, status, disconnect };
157
164
  };
158
165
 
@@ -1,9 +1,18 @@
1
1
  // In memory pubsub, for either client-only testing, or server-websocket testing
2
2
  const { v4:uuidv4 } = await import('uuid');
3
3
  const { TextEncoder, crypto, Buffer } = globalThis;
4
+ const publicationRolloverLimit = 1000;
4
5
 
5
6
  function setBucket(collection, type, topicId, subject, value) { // Set value in the collection.
6
7
  const bucket = collection[type][topicId] ||= {};
8
+ if ((collection === data) && (type === 'pub')) {
9
+ const keys = Object.keys(bucket);
10
+ const size = keys.length;
11
+ if (size >= publicationRolloverLimit) {
12
+ console.warn('Over pub limit on topic', topicId, size); // TODO: rotate out the earliest received.
13
+ delete bucket[keys[0]];
14
+ }
15
+ }
7
16
  bucket[subject] = value;
8
17
  }
9
18
  function removeBucket(collection, type, topicId, subject) { // Return the value and stop storing it.
@@ -20,7 +29,7 @@ const PUBLISH_TIMEOUT = 24 * 60 * 60e3; // Delete after 24 hours.
20
29
  const timeouts = {pub: {}, sub: {}};
21
30
  function expire(type, topicId, subject, remover, timeout) { // Cancellably schedule remover() to fire at timeout.
22
31
  if (!timeout) return;
23
- setBucket(timeouts, type, topicId, subject, setTimeout(remover, timeout));
32
+ setBucket(timeouts, type, topicId, subject, setTimeout(() => { remover(); cancel(type, topicId, subject); }, timeout));
24
33
  }
25
34
  function cancel(type, topicId, subject) { // Cancel a sheduled expiration.
26
35
  clearTimeout(removeBucket(timeouts, type, topicId, subject));
@@ -46,25 +55,34 @@ function normalizeTopic({name, region, owner, write = 'open'} = {}) {
46
55
  function deriveTopicId(topic) {
47
56
  return JSON.stringify(normalizeTopic(topic)); // No need to hash in this implementation.
48
57
  }
58
+ function delay(ms = 0) {
59
+ return ms && new Promise(resolve => setTimeout(resolve, ms));
60
+ }
49
61
 
50
62
  let invoke;
51
63
  export function setReceiver(receiver) {
52
64
  invoke = receiver;
53
65
  }
66
+ const throttleMS = 30; // Just to yield to other stuff.
67
+ async function pauseInvoke(...rest) {
68
+ invoke(...rest);
69
+ await new Promise(resolve => setTimeout(resolve, throttleMS));
70
+ }
54
71
 
55
- export function subscribe(topicName, nodeTag, {since = 'all'}) {
72
+ export async function subscribe(topicName, nodeTag, {since = 'all'}) {
56
73
  // Axona allows multiple handlers on the same topic, but we don't use that in civildefense, and do not implement it here.
57
74
  const topicId = deriveTopicId(topicName);
58
75
  const id = uuidv4();
59
76
  cancel('sub', topicId, id);
60
77
  expire('sub', topicId, id, () => deleteSub(topicId, id), SUBSCRIPTION_TIMEOUT);
61
78
  setBucket(data, 'sub', topicId, nodeTag, id);
62
- if (since) setTimeout(() => { // invoke handler on any sticky data, but only after we have told client the subscription id.
79
+ if (since) { // invoke handler on any sticky data, but only after we have told client the subscription id.
80
+ await delay(100);
63
81
  let lastEnvelope = null, lastTime = 0;
64
82
  for (const envelope of getDataValues('pub', topicId)) {
65
83
  switch (since) {
66
84
  case 'all':
67
- invoke(nodeTag, id, envelope);
85
+ await pauseInvoke(nodeTag, id, envelope);
68
86
  break;
69
87
  case 'latest':
70
88
  if (envelope.ts > lastTime) {
@@ -73,11 +91,11 @@ export function subscribe(topicName, nodeTag, {since = 'all'}) {
73
91
  }
74
92
  break;
75
93
  default: // Must be a timestamp
76
- if (envelope.ts === since) invoke(nodeTag, id, envelope);
94
+ if (envelope.ts === since) await pauseInvoke(nodeTag, id, envelope);
77
95
  }
78
96
  }
79
97
  if (lastEnvelope) invoke(nodeTag, id, lastEnvelope);
80
- }, 100);
98
+ }
81
99
  return {topicName, topicId, id};
82
100
  }
83
101
 
@@ -92,7 +110,10 @@ export function deleteSubscriber(nodeTag) {
92
110
  for (const topicId in data.sub) {
93
111
  const keySubs = data.sub[topicId];
94
112
  for (const [subject, value] of Object.entries(keySubs)) {
95
- if (nodeTag === subject) deleteSub(topicId, subject, keySubs);
113
+ if (nodeTag === subject) {
114
+ deleteSub(topicId, subject, keySubs);
115
+ cancel('sub', topicId, subject);
116
+ }
96
117
  }
97
118
  }
98
119
  }
@@ -106,19 +127,19 @@ export async function publish(topic, message, {signWith}) {
106
127
  const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
107
128
  const msgId = toHex(new Uint8Array(hash));
108
129
  const envelope = {msgId, topic, ts: Date.now(), message, signerPubkey};
109
- for (const [nodeTag, id] of getDataEntries('sub', topicId)) invoke(nodeTag, id, envelope);
130
+ for (const [nodeTag, id] of getDataEntries('sub', topicId)) await pauseInvoke(nodeTag, id, envelope);
110
131
  setBucket(data, 'pub', topicId, msgId, envelope);
111
132
  expire('pub', topicId, msgId, () => removeBucket(data, 'pub', topicId, msgId), PUBLISH_TIMEOUT);
112
133
  return msgId;
113
134
  }
114
135
 
115
- export function unpublish(topic, msgId, {signWith}) {
136
+ export async function unpublish(topic, msgId, {signWith}) {
116
137
  const topicId = deriveTopicId(topic);
117
138
  cancel('pub', topicId, msgId);
118
139
  const envelope = removeBucket(data, 'pub', topicId, msgId);
119
140
  if (!envelope) return {ok: false}; // we didn't have it.
120
141
  envelope.deleted = true;
121
142
  envelope.message = null;
122
- for (const [nodeTag, id] of getDataEntries('sub', topicId)) invoke(nodeTag, id, envelope);
143
+ for (const [nodeTag, id] of getDataEntries('sub', topicId)) await pauseInvoke(nodeTag, id, envelope);
123
144
  return {ok: true};
124
145
  }
@@ -1,6 +1,7 @@
1
1
  import { s2, s1, r1 } from 's2js';
2
2
  const { cellid, LatLng, Point, Cell, Cap, RegionCoverer } = s2;
3
3
  import { cellHex } from './versions.js';
4
+ import { geoCellCenter/*, geoCellId*/ } from '@axona/protocol';
4
5
  const { BigInt } = globalThis;
5
6
 
6
7
  // s2 defines non-overlapping cells that completely cover the globe, at several different levels of cell-size.
@@ -12,43 +13,59 @@ const { BigInt } = globalThis;
12
13
  // Meanwhile as the user changes the area being shown, we subscribe to whatever cells we need in order to cover the display
13
14
  // area without overlapping cells.
14
15
 
15
- export const MIN_LEVEL = 3; // Corresponds to the top level Axona regions.
16
+ export const MIN_LEVEL = 3; // The largest cell that is still smaller than an Axona region.
16
17
  const MAX_S2_LEVEL = 30; // The leaf level that Cell.fromPoint operates at.
17
18
  const MAX_MAP_LEVEL = 17; // The max level that findCoverCellsByCenterAndRadius will use on our maps.
18
19
 
19
20
  const EARTH_RADIUS_METERS = 6371e3;
20
21
 
21
22
  export function getPointInCell(cellId) { // answer [lat, lng] in degrees from a BigInt
23
+ // const {lat, lng} = geoCellCenter(cellid);
24
+ // return [lat, lng];
22
25
  // CAUTION: This is intended for s2 level 3 or finer, and won't always work symmetrically for our top-level regions.
23
26
  // e.g. getContainingCells(...getPointInCell( cell for 0x47 )][0] is 0x46!
24
27
  let center = s2.cellid.latLng(cellId);
25
28
  return [s1.angle.degrees(center.lat), s1.angle.degrees(center.lng)];
26
29
  }
27
30
 
28
- function getCellSubdivision(cell) {
31
+ function getCellSubdivision(cell) { // Answer four children off BigInt cell, as BigInt.
29
32
  return cellid.children(cell);
30
33
  }
31
- function getCellLevel(cell) {
34
+ export function getSubdivision(hexString) { // Same as getCellSubDivision, but accepting and returning hex strings.
35
+ return getCellSubdivision(BigInt('0x' + hexString)).map(childCell => cellHex(childCell));
36
+ }
37
+ function getCellLevel(cell) { // Answer level of BigInt cell, as number.
32
38
  return cellid.level(cell);
33
39
  }
34
- function getFace(cell) {
40
+ function getCellFace(cell) { // Answer top level face id of BigInt cell, as integer 0 through 5.
35
41
  return cellid.face(cell);
36
42
  }
37
- export function getSubdivision(hexString) {
38
- return getCellSubdivision(BigInt('0x' + hexString)).map(childCell => cellHex(childCell));
43
+ export function cellContains(putativeOuter, putativeInner) { // Answer true IFF putativeOuter BigInt cell contains putativeInner
44
+ return cellid.contains(putativeOuter, putativeInner);
45
+ }
46
+ export function pointFromLatLng(lat, lng) { // Answer an s2 Point from lat/lng in degrees.
47
+ const latLng = LatLng.fromDegrees(lat, lng);
48
+ return Point.fromLatLng(latLng);
49
+ }
50
+ export function cellFromCellID(bigint) {
51
+ return Cell.fromCellID(bigint);
52
+ }
53
+ export function getSmallestCellId(lat, lng, level = MAX_MAP_LEVEL) { // Answer smallest BigInt cellid containing latitude/longitude in degrees, at integer level.
54
+ const userPt = pointFromLatLng(lat, lng);
55
+ const userLocCellId = Cell.fromPoint(userPt).id; // This is at MAX_S2_LEVEL
56
+ return cellid.parent(userLocCellId, level);
39
57
  }
40
58
 
41
- // Return a list of the cell ids that contain the point.
59
+ // Return a list of the cell ids that contain the point, from region to MAX_S2_LEVEL
60
+ // Note that the first of the cells (the region
42
61
  export function getContainingCells(lat, lng) {
43
- const userLatLng = LatLng.fromDegrees(lat, lng);
44
- const userPt = Point.fromLatLng(userLatLng);
45
- // Get leaf-level CellId (level 30)
46
- const userLocCellId = Cell.fromPoint(userPt).id; // This is at level 30.
47
- let cells = Array(MAX_S2_LEVEL);
48
- for (let level = 0; level <= MAX_S2_LEVEL; level++) { // This would be more efficient going backwards using immediateParent, but who cares.
49
- cells[level] = cellid.parent(userLocCellId, level);
62
+ let id = getSmallestCellId(lat, lng);
63
+ const level = MAX_MAP_LEVEL, cells = Array(MAX_MAP_LEVEL + 1 - MIN_LEVEL);
64
+ for (let index = level - MIN_LEVEL; index >= 0; index--) {
65
+ cells[index] = id;
66
+ id = cellid.immediateParent(id);
50
67
  }
51
- return cells.slice(MIN_LEVEL, MAX_MAP_LEVEL + 1); // We can only make use between Axona region size and the smallest region our maps subscribe to.
68
+ return cells;
52
69
  }
53
70
 
54
71
  export function findCoverCellsByMinMaxLatLng({minLat, maxLat, minLng, maxLng, full = false,
@@ -58,8 +75,8 @@ export function findCoverCellsByMinMaxLatLng({minLat, maxLat, minLng, maxLng, fu
58
75
 
59
76
  // There are a lot of ways that seem like they do this, but it is very easy to find something that works for a few cases,
60
77
  // but misses cells in some circumstances, so be wary about rewriting this.
61
- const lo = s2.LatLng.fromDegrees(minLat, Math.max(minLng, -180));
62
- const hi = s2.LatLng.fromDegrees(maxLat, Math.max(maxLng, 190));
78
+ const lo = LatLng.fromDegrees(minLat, minLng);
79
+ const hi = LatLng.fromDegrees(maxLat, maxLng);
63
80
 
64
81
  const rect = full ? s2.Rect.fullRect() : new s2.Rect(
65
82
  new r1.Interval(lo.lat, hi.lat),
@@ -69,4 +86,3 @@ export function findCoverCellsByMinMaxLatLng({minLat, maxLat, minLng, maxLng, fu
69
86
  const coverer = new RegionCoverer({minLevel, maxLevel, maxCells});
70
87
  return coverer.covering(rect); // a CellUnion — array-like of bigint cell IDs, already normalized/minimal
71
88
  }
72
-
@@ -25,10 +25,13 @@ export function cellHex(cellid) { // Convert cellid BigInt to properly padded he
25
25
  export function alertTopic(cellid, tag) { // Return topic name for public info about specified tag in cellid.
26
26
  return `civildefense.io:${dataVersion}:${cellHex(cellid)}:${canonicalTag(tag)}`;
27
27
  }
28
- export function topicCell(topicName) { // Return the cell that is baked in to the topic.
28
+ export function topicCellHex(topicName) { // Return the hex string cell that is baked in to the topic.
29
29
  // Note that canonicalTag(tag) may contain a colon, but dataVersion must not.
30
30
  return topicName.match(/civildefense.io:[^:]+:([a-z0-9]+):/)[1];
31
31
  }
32
+ export function topicCell(topicName) { // Return the BigInt cell that is baked in to the topic.
33
+ return BigInt('0x' + topicCellHex(topicName));
34
+ }
32
35
  export function topicRegion(topicName) { // Return the region that is baked in to the topic.
33
- return '0x' + topicCell(topicName).slice(0, 2);
36
+ return '0x' + topicCellHex(topicName).slice(0, 2);
34
37
  }
@@ -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.5.21';
3
+ const serviceVersion = '4.5.23';
4
4
 
5
5
  const cacheList = [ // The files we need.
6
6
  "/",
@@ -124,6 +124,8 @@ async function cacheFirst({request, event}) {
124
124
  try {
125
125
  const responseFromNetwork = await fetch(request);
126
126
  if (request.method !== 'GET' || request.cache === 'no-store') return responseFromNetwork; // Cache shouldn't allow anyway.
127
+ const mime = responseFromNetwork.headers.get("Content-Type");
128
+ if (/^(audio|video)\//.test(mime)) return responseFromNetwork; // Do not cache streamable media.
127
129
  // Put clone of response in cache (so that original can be returned.
128
130
  // Tell event to keep worker open while we put it, even though we return response immediately.
129
131
  const cache = await caches.open(serviceVersion);
@@ -254,6 +254,7 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
254
254
  .alert-marker {
255
255
  text-shadow: 8px -3px 10px rgb(0 0 0 / 60%);
256
256
  animation: pulse ease 0.8s 1;
257
+ transition: transform 0.8s, opacity 0.4s;
257
258
  }
258
259
  .alert-commented {
259
260
  display: none;
@@ -271,6 +272,30 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
271
272
  left: 5px;
272
273
  text-align: center;
273
274
  }
275
+ .aggregate.alert-pin {
276
+ font-size: 60px;
277
+ line-height: 60px;
278
+ top: -10px;
279
+ left: -10px;
280
+ transition: font-size 0.8s;
281
+ }
282
+ .aggregate.alert-pin:after {
283
+ content: "";
284
+ width: 70px;
285
+ height: 70px;
286
+ position: absolute;
287
+ top: -5px;
288
+ left: -5px;
289
+ background-image: radial-gradient(transparent 30%, var(--md-sys-color-primary), transparent 70%);
290
+ transition: opacity 0.8s;
291
+ animation: pulse ease 0.8s 1;
292
+ }
293
+ .aggregate.alert-pin.starting {
294
+ font-size: 0px;
295
+ }
296
+ .aggregate.alert-pin.starting:after {
297
+ opacity: 0;
298
+ }
274
299
  @keyframes pulse {
275
300
  0% { filter: saturate(1); }
276
301
  25% { filter: saturate(4); }
@@ -303,9 +328,29 @@ button, .leaflet-control-zoom > a, .leaflet-popup-close-button, md-outlined-icon
303
328
  md-filter-chip {
304
329
  --md-sys-color-surface-container-low: #B8C2D7; /* non-selected elevated filter chip */
305
330
  }
306
- md-filter-chip minidenticon-svg + md-icon-button {
331
+ md-filter-chip minidenticon-svg + md-icon-button,
332
+ md-filter-chip .identicon + md-icon-button {
307
333
  bottom: 10px;
308
334
  }
335
+ md-filter-chip .identicon md-icon {
336
+ font-size: 18px;
337
+ width: 20px;
338
+ position: relative;
339
+ bottom: 3px;
340
+ }
341
+ md-filter-chip .identicon {
342
+ width: 43px;
343
+ }
344
+ md-filter-chip .identicon minidenticon-svg {
345
+ width: 18px;
346
+ display: inline-block;
347
+ position: relative;
348
+ bottom: 5px;
349
+ left: -3px;
350
+ }
351
+ md-filter-chip minidenticon-svg {
352
+ background: white;
353
+ }
309
354
  md-filter-chip md-icon-button {
310
355
  position: relative;
311
356
  bottom: 3px;
@@ -323,9 +368,6 @@ md-filter-chip:not([selected]) md-icon::after {
323
368
  md-filter-chip.pub md-icon {
324
369
  color: var(--md-sys-color-secondary);
325
370
  }
326
- md-filter-chip minidenticon-svg {
327
- background: white;
328
- }
329
371
  md-outlined-icon-button minidenticon-svg, md-outlined-icon-button img {
330
372
  height: 40px;
331
373
  width: 40px;
package/server/app.js CHANGED
@@ -14,18 +14,33 @@ const argv = yargs(hideBin(process.argv))
14
14
  .option('nPortals', {
15
15
  alias: 'p',
16
16
  type: 'number',
17
- default: Math.max(2, logicalCores - 3),
17
+ default: process.env.PORTALS ? parseInt(process.env.PORTALS) : Math.max(2, logicalCores - 3),
18
18
  description: "The number of steady nodes that handle initial connections."
19
19
  })
20
+ .option('regionStartIndex', {
21
+ type: 'number',
22
+ default: -1,
23
+ description: "If non-negative, place each of the nPortals in successive regions (excluding excludedRegionFromSeries) starting with the region at regionStaartIndex."
24
+ })
25
+ .option('excludedRegionFromSeries', {
26
+ type: 'string', array: true,
27
+ default: ['80', '88', '89'],
28
+ description: "If regionStartIndex is used, the ordered list of regions to occupy is the list of canonical regions less these."
29
+ })
20
30
  .option('baseURL', {
21
31
  type: 'string',
22
- default: 'http://localhost:3000',
32
+ default: process.env.BRIDGE_URL || 'wss://bridge.axona.net',
23
33
  description: "The base URL of the portal server through which to bootstrap."
24
34
  })
25
- .option('externalBaseURL', {
26
- type: 'string',
27
- default: '',
28
- description: "The base URL of the some other portal server to which we should connect ours, if any."
35
+ // .option('externalBaseURL', {
36
+ // type: 'string',
37
+ // default: '',
38
+ // description: "The base URL of the some other portal server to which we should connect ours, if any."
39
+ // })
40
+ .option('port', {
41
+ type: 'number',
42
+ default: 3000,
43
+ description: "Port for the server to listen on."
29
44
  })
30
45
  .option('announce', {
31
46
  type: 'boolean',
@@ -61,7 +76,6 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
61
76
  const logger = (await import('morgan')).default;
62
77
  const { configureWebsocket } = await import('./websocket.js');
63
78
 
64
- const port = parseInt((new URL(argv.baseURL)).port || '80');
65
79
  process.title = 'yz.social';
66
80
  const app = express();
67
81
  app.use(logger(':date[iso] :status :method :url :res[content-length] - :response-time ms'));
@@ -101,8 +115,8 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
101
115
  extensions: ['js'] // Some dependencies refer to .js files as relative pathnames, with the .js missing.
102
116
  }));
103
117
 
104
- server.listen(port);
105
- log(`Listening on ${port} and starting ${argv.nPortals} nodes on ${logicalCores} ${cpus()[0].model} logical cores.`);
118
+ server.listen(argv.port);
119
+ log(`Listening on ${argv.port} and starting ${argv.nPortals} nodes on ${logicalCores} ${cpus()[0].model} logical cores.`);
106
120
  for (let i = 0; i < argv.nPortals; i++) {
107
121
  cluster.fork();
108
122
  await delay(); // Number chosen to give an unspikey rise in packets/s.
@@ -118,8 +132,14 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
118
132
  } else {
119
133
  process.title = 'axona-starting';
120
134
  const { P2PWebNetwork, location } = await import('../index.js');
135
+ const { MAJORS, geoCellCenter } = await import('@axona/protocol');
136
+ const excluded = argv.excludedRegionFromSeries.map(hex => parseInt(hex, 16).toString());
137
+ const regions = Object.keys(MAJORS).filter(r => !excluded.includes(r));
138
+ const index = (cluster.worker.id - 1 + argv.regionStartIndex) % regions.length;
139
+ const region = (argv.regionStartIndex >= 0) && regions[index];
140
+ const loc = region ? geoCellCenter(parseInt(region)) : location;
121
141
  const network = await P2PWebNetwork.create({
122
- region: location,
142
+ location: loc,
123
143
  infoLogger: log,
124
144
  debugLogger: debug
125
145
  });
@@ -31,6 +31,7 @@ export function configureWebsocket(server) {
31
31
  ws.on('error', console.error);
32
32
  ws.on('close', () => {
33
33
  console.log('Disconnected', nodeTag);
34
+ delete sockets[nodeTag];
34
35
  operator.deleteSubscriber(nodeTag);
35
36
  });
36
37
  });
@@ -42,5 +43,5 @@ export function configureWebsocket(server) {
42
43
  ws.ping();
43
44
  return null;
44
45
  });
45
- }, 30e3);
46
+ }, 20e3);
46
47
  }