@yz-social/civildefense.io 4.4.1
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/README.md +68 -0
- package/announce.js +24 -0
- package/docs/YZ-Brief.pdf +0 -0
- package/index.js +2 -0
- package/movie/camera.jpg +0 -0
- package/movie/movie.js +151 -0
- package/movie/script.js +272 -0
- package/movie/test.js +2 -0
- package/nginx/nginx.conf +83 -0
- package/nginx/yz.social +81 -0
- package/package.json +34 -0
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -0
- package/public/about/CivilDefense.mp4 +0 -0
- package/public/about/In case of Nazis, use CivilDefense.io.png +0 -0
- package/public/about/broadcast.png +0 -0
- package/public/about/civil-defense.png +0 -0
- package/public/about/conversation-and-image-high.png +0 -0
- package/public/about/conversation-and-image.png +0 -0
- package/public/about/en.html +157 -0
- package/public/about/es.html +142 -0
- package/public/about/flood-fire-ice-high.png +0 -0
- package/public/about/flood-fire-ice.png +0 -0
- package/public/about/hero-high.png +0 -0
- package/public/about/hero.png +0 -0
- package/public/about/index.html +8 -0
- package/public/about/nazis.png +0 -0
- package/public/about/script.js +69 -0
- package/public/about/streaming-radio-high.png +0 -0
- package/public/about/streaming-radio.png +0 -0
- package/public/about/style.css +306 -0
- package/public/control-safe-rectangle.html +40 -0
- package/public/favicon.ico +0 -0
- package/public/images/Achtung.png +0 -0
- package/public/images/YZ Owl.png +0 -0
- package/public/images/civil-defense-122.png +0 -0
- package/public/images/civil-defense-192.png +0 -0
- package/public/images/civil-defense-240.png +0 -0
- package/public/images/civil-defense-512.png +0 -0
- package/public/images/civil-defense.png +0 -0
- package/public/images/hero-small.png +0 -0
- package/public/images/qr-scan.svg +2 -0
- package/public/images/qr.png +0 -0
- package/public/images/qr.svg +155 -0
- package/public/images/recenter.svg +5 -0
- package/public/images/share.png +0 -0
- package/public/images/share.svg +2 -0
- package/public/index.html +170 -0
- package/public/javascripts/agent.js +324 -0
- package/public/javascripts/display.js +25 -0
- package/public/javascripts/hashtags.js +205 -0
- package/public/javascripts/main.js +394 -0
- package/public/javascripts/map.js +725 -0
- package/public/javascripts/p2pWebNetwork.js +245 -0
- package/public/javascripts/s2.js +77 -0
- package/public/javascripts/scripting.js +112 -0
- package/public/javascripts/service-manager.js +149 -0
- package/public/javascripts/translations.js +139 -0
- package/public/javascripts/versions.js +23 -0
- package/public/manifest.json +25 -0
- package/public/owl.ico +0 -0
- package/public/platformer.html +52 -0
- package/public/robots.txt +6 -0
- package/public/service-worker.js +218 -0
- package/public/stylesheets/style.css +442 -0
- package/routes/index.js +123 -0
- package/server/app.js +116 -0
- package/server/bridge.js +397 -0
- package/server/dirname.js +15 -0
- package/server/getLocation.js +18 -0
- package/server/identity.js +111 -0
- package/server/location.json +12 -0
- package/spec/axonSpec.gratuitousNameChangeForSignal +246 -0
- package/spec/axonSpec.js +280 -0
- package/spec/axonSpec.jsRemoveThePartAfterJS +252 -0
- package/spec/civildefenseSpec.js +61 -0
- package/spec/pubsubSpec.js +128 -0
- package/spec/support/jasmine.mjs +14 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/*
|
|
2
|
+
FIXME: Things that either don't pass, or require undocumented workarounds.
|
|
3
|
+
TODO: Things that ought to be dealt with at some point, but can be deferred until later.
|
|
4
|
+
CURRENTLY:
|
|
5
|
+
- This passes (with the FIXMEs in place) in main/3.8.0
|
|
6
|
+
- This usually fails to receive some of the expected subscription callbacks in testnet/4.3.2, and thus hangs.
|
|
7
|
+
|
|
8
|
+
To RUN, e.g., in NodeJS:
|
|
9
|
+
- You may need to adjust the path to webTransport. See the first TODO entry.
|
|
10
|
+
- To switch between them, don't forget to change the wss url a few lines down from hehre.
|
|
11
|
+
- Have jasmine or the like installed and initialized, and then e.g., npx jasmine spec/axonSpec.js.
|
|
12
|
+
|
|
13
|
+
It is worth running this several times. It sometimes works once, and then fails or has enormous connect times on another run.
|
|
14
|
+
|
|
15
|
+
The logging tells the story.
|
|
16
|
+
Alice, Bob, and Carol are Node instances. (Defined below, followed by the Jasmine test suite.)
|
|
17
|
+
Alice and Bob will subscribe and publish to an open/since:'all' topic.
|
|
18
|
+
Carol will join and subscribe between the previous subscriptions and their publications.
|
|
19
|
+
After publications, Bob will politely disconnect, and then restart and subscribe again to get same results.
|
|
20
|
+
Carol will restart without an explicit disconnect, and subscribe again after publications.
|
|
21
|
+
David will join and subscribe after publications.
|
|
22
|
+
*/
|
|
23
|
+
const { describe, it, expect, beforeAll, afterAll, BigInt } = globalThis;
|
|
24
|
+
import { AxonaPeer, AxonaDomain, NeuronNode, createNodeIdentity, createAuthorIdentity, regionCenter, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION} 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/src/transport/web/index.js';
|
|
28
|
+
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
|
|
29
|
+
|
|
30
|
+
class Node { // Stuff we have to do every time. TODO: build something like this into Axona.
|
|
31
|
+
static version = KERNEL_VERSION;
|
|
32
|
+
log(...rest) {
|
|
33
|
+
console.log(new Date(), this.label, this.transportIdentity.id.slice(0, 10), ...rest);
|
|
34
|
+
}
|
|
35
|
+
static async create({location, transportIdentity, bridgeUrl = 'wss://testnet.axona.net',
|
|
36
|
+
label = 'network', store, authorIdentity,
|
|
37
|
+
synapseCount = 4, timeoutMs = 10e3, ...rest} = {}) {
|
|
38
|
+
// 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
|
+
if (typeof(authorIdentity) === 'string') // TODO: this is pretty awkward.
|
|
48
|
+
authorIdentity = createAuthorIdentity({persistAs: label, store: {get() { return authorIdentity; }}});
|
|
49
|
+
authorIdentity ||= createAuthorIdentity({persistAs: label, store});
|
|
50
|
+
authorIdentity = await authorIdentity;
|
|
51
|
+
|
|
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});
|
|
58
|
+
|
|
59
|
+
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});
|
|
63
|
+
return self;
|
|
64
|
+
}
|
|
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
|
+
async disconnect() { // Politely close network connection.
|
|
89
|
+
const start = Date.now();
|
|
90
|
+
const health = this.health();
|
|
91
|
+
await this.peer.leave();
|
|
92
|
+
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
|
+
}
|
|
95
|
+
async subscribe({eventName, region, owner, since = 'all', handler}) { // Assign handler for eventName, or remove any handler if falsy.
|
|
96
|
+
const topic = {region, name: eventName};
|
|
97
|
+
if (owner) topic.owner = owner;
|
|
98
|
+
if (handler) {
|
|
99
|
+
const callback = async ({...rest}) => handler({receiver: this, ...rest}); // Add receiver to envelope.
|
|
100
|
+
await this.peer.sub(topic, callback, {since});
|
|
101
|
+
} else {
|
|
102
|
+
this.peer.unsub(topic, {});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async publish({eventName, region, owner, signWith = this.authorIdentity, killTag, message}) {
|
|
106
|
+
// Publish data to subscribers of eventName.
|
|
107
|
+
const topic = {region, name: eventName};
|
|
108
|
+
if (owner) topic.owner = owner;
|
|
109
|
+
const options = {signWith};
|
|
110
|
+
if (message) return await this.peer.pub(topic, message, options);
|
|
111
|
+
if (!killTag) return null;
|
|
112
|
+
return await this.peer.kill(topic, killTag, options);
|
|
113
|
+
}
|
|
114
|
+
static regionCode(lat, lng) { // Answer containing region code.
|
|
115
|
+
return geoCellId(lat, lng);
|
|
116
|
+
}
|
|
117
|
+
static canonicalizeRegion(lat, lng) {
|
|
118
|
+
// Answer a {lat, lng} that is the center of a top-level Axona region containing the given {lat, lng}.
|
|
119
|
+
// E.g., a precise location gets anonymized to containing top-level cell center.
|
|
120
|
+
return geoCellCenter(this.regionCode(lat, lng));
|
|
121
|
+
}
|
|
122
|
+
static delay(ms, result) { // Promise result after ms milliseconds.
|
|
123
|
+
return new Promise(resolve => setTimeout(resolve, ms, result));
|
|
124
|
+
}
|
|
125
|
+
get synaptomeSize() { // Safely answer the number of connections.
|
|
126
|
+
return this.node.synaptome?.size ?? 0;
|
|
127
|
+
}
|
|
128
|
+
health() { // from peer
|
|
129
|
+
return this.peer.health();
|
|
130
|
+
}
|
|
131
|
+
host() { // through peer
|
|
132
|
+
return this.peer.host();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe("CivilDefense", function () {
|
|
137
|
+
// These two are not acceptance criteria for how long the operation should take, but are instead
|
|
138
|
+
// how long we are willing to wait in in the test before assuming it is never going to happen.
|
|
139
|
+
const connectAllowanceMS = 20e3;
|
|
140
|
+
const deliveryAllowanceMS = 20e3;
|
|
141
|
+
const location = regionCenter('uscentlw'); // FIXME 'uswest');
|
|
142
|
+
|
|
143
|
+
let alice, bob, carol, david, emma;
|
|
144
|
+
let aliceKillTag, currentOperation;
|
|
145
|
+
|
|
146
|
+
const version = Date.now(); // Each run gets a new topic.
|
|
147
|
+
const eventName = `civilDefense.io:${version}:pubsub-test`;
|
|
148
|
+
const regionCode = Node.regionCode(location.lat, location.lng);
|
|
149
|
+
class TestNode extends Node { // Version of Node specific to these tests.
|
|
150
|
+
async publish({message, killTag, ...rest}) { // Log publish, and default eventName/region for these tests.
|
|
151
|
+
const published = await super.publish({eventName, region:regionCode, message, killTag, ...rest});
|
|
152
|
+
this.log(killTag ? 'kill' : 'publish', currentOperation, message || killTag);
|
|
153
|
+
return published;
|
|
154
|
+
}
|
|
155
|
+
resetExpectedPromise(expectedCount) {
|
|
156
|
+
// Sets a current internal handler, without updating subscription, which records handled messages in this.events[currentOperation].
|
|
157
|
+
// Returns a ready promise that resolves when the expectedCount has been received.
|
|
158
|
+
return this.ready = new Promise(resolve => {
|
|
159
|
+
const handlerTime = Date.now();
|
|
160
|
+
this.handler = ({message, receiver, ts:pubTime}) => { // Ensure that the receiver's events[currentOperation] is a list, and push message on to it.
|
|
161
|
+
// FIXME: ts is undefined for a kill, which is weird:
|
|
162
|
+
// 1. I would think that Axona needs the time in order to dedupe and order properly?
|
|
163
|
+
// 2. The app may need the time, especially since we are not reliably getting events in ts order. (See "wrong order" comment, below.)
|
|
164
|
+
pubTime ||= 0;
|
|
165
|
+
const start = Math.max(pubTime, handlerTime);
|
|
166
|
+
const elapsed = Date.now() - start;
|
|
167
|
+
const data = receiver.events[currentOperation] ||= [];
|
|
168
|
+
receiver.log(`received ${currentOperation} ${data.length} ${message} after ${elapsed.toLocaleString()} ms.`);
|
|
169
|
+
data.push(message || null);
|
|
170
|
+
if (data.length < expectedCount) return;
|
|
171
|
+
resolve();
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async subscribe({expectedCount = 0, handler = (...rest) => this.handler?.(...rest), ...rest} = {}) {
|
|
176
|
+
const ready = this.resetExpectedPromise(expectedCount);
|
|
177
|
+
const subscribed = await super.subscribe({eventName, region:regionCode, handler});
|
|
178
|
+
this.log('subscribed, expecting', expectedCount);
|
|
179
|
+
return subscribed;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const inOrder = ['alice pub', ' bob pub'];
|
|
183
|
+
|
|
184
|
+
beforeAll(async function () {
|
|
185
|
+
[alice, bob] = await Promise.all([
|
|
186
|
+
TestNode.create({location, events: {}, label: 'alice', authorIdentity: '{"kind":"author","pubkey":"b08988518013fabc5949353281d263cb7916f273f3f8badf0b16443a67d0b05c","privkey":"MC4CAQAwBQYDK2VwBCIEIC7ZrU8C2WSBdoBQW/JoE0ZYRkp/kFcCGf7H25DVqM17","createdAt":1782508727637}'}),
|
|
187
|
+
TestNode.create({location, events: {}, label: ' bob', authorIdentity: '{"kind":"author","pubkey":"6447bae1e8f1d5c99243251b43b4d354fb4928c10b69076563617c06fff46ca3","privkey":"MC4CAQAwBQYDK2VwBCIEIGGHVqedJBPtzSDR3P03HHpWTPlyXteHJAPtABmdwujM","createdAt":1782508849433}'})
|
|
188
|
+
]);
|
|
189
|
+
await Promise.all([alice.subscribe({expectedCount: 2}), bob.subscribe({expectedCount: 2})]);
|
|
190
|
+
// Now carol joins and subscribes.
|
|
191
|
+
carol = await TestNode.create({location, events: {}, label: 'carol', authorIdentity: '{"kind":"author","pubkey":"9fac71086211be29c685fc8aeab7725f4e78a0476482b817b9160273a40812e8","privkey":"MC4CAQAwBQYDK2VwBCIEILSc0paO923M0X8d8ux6JkjEFz65gk2BVBPlTtlZD6Q3","createdAt":1782508860832}'});
|
|
192
|
+
await carol.subscribe({expectedCount: 2});
|
|
193
|
+
|
|
194
|
+
currentOperation = 'initial';
|
|
195
|
+
// 'alice pub' starts and completes before 'bob pub' starts.
|
|
196
|
+
aliceKillTag = await alice.publish({message: 'alice pub'});
|
|
197
|
+
await TestNode.delay(500); // FIXME: without this delay, subscription handlers are called in the wrong order.
|
|
198
|
+
await bob.publish({message: ' bob pub'});
|
|
199
|
+
await Promise.all([alice.ready, bob.ready, carol.ready]);
|
|
200
|
+
}, 2 * connectAllowanceMS + deliveryAllowanceMS);
|
|
201
|
+
describe("initial", function () {
|
|
202
|
+
it("alice receives all pubs from herself and bob", function () { expect(alice.events.initial).toEqual(inOrder); });
|
|
203
|
+
it(" bob receives all pubs from himself and bob", function () { expect( bob.events.initial).toEqual(inOrder); });
|
|
204
|
+
it("carol receives all pubs from alice and bob", function () { expect(carol.events.initial).toEqual(inOrder); });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("restart", function () {
|
|
208
|
+
beforeAll(async function () {
|
|
209
|
+
await bob.disconnect();
|
|
210
|
+
carol.handler = null; // We're not going to disconnect this instance of carol, but let's not get confused by subscription callbacks.
|
|
211
|
+
[bob, carol, david] = await Promise.all([
|
|
212
|
+
TestNode.create({location, events: bob.events, label: ' bob', authorIdentity: bob.authorIdentity}),
|
|
213
|
+
TestNode.create({location, events: carol.events, label: 'carol', authorIdentity: carol.authorIdentity}),
|
|
214
|
+
TestNode.create({location, events: {}, label: 'david', authorIdentity: '{"kind":"author","pubkey":"4071c632d8aedfeee7293c23a539b368211cf903722d12d521eeda226047e1ed","privkey":"MC4CAQAwBQYDK2VwBCIEIN/rAuWAlhP2blF3neaoXxLM727hQ1ZzR0hsg9yYS6hd","createdAt":1782508870567}'})
|
|
215
|
+
]);
|
|
216
|
+
currentOperation = 'restart';
|
|
217
|
+
await Promise.all([bob.subscribe({expectedCount: 2}), carol.subscribe({expectedCount: 2}), david.subscribe({expectedCount: 2})]);
|
|
218
|
+
await Promise.all([bob.ready, carol.ready, david.ready]);
|
|
219
|
+
}, 1 * connectAllowanceMS + deliveryAllowanceMS);
|
|
220
|
+
it(" bob receives all pubs from himself and bob", function () { expect( bob.events.restart).toEqual(inOrder); });
|
|
221
|
+
it("carol receives all pubs from alice and bob", function () { expect(carol.events.restart).toEqual(inOrder); });
|
|
222
|
+
it("david receives all pubs from alice and bob", function () { expect(david.events.restart).toEqual(inOrder); });
|
|
223
|
+
|
|
224
|
+
describe("after kill", function () {
|
|
225
|
+
beforeAll(async function () {
|
|
226
|
+
currentOperation = 'kill';
|
|
227
|
+
alice.resetExpectedPromise(1);
|
|
228
|
+
bob.resetExpectedPromise(1);
|
|
229
|
+
carol.resetExpectedPromise(1);
|
|
230
|
+
david.resetExpectedPromise(1);
|
|
231
|
+
await alice.publish({killTag: aliceKillTag});
|
|
232
|
+
await TestNode.delay(500); // FIXME: without this delay, someone already subscribed (like Bob) sometimes (rarely) doesn't get the kill callback.
|
|
233
|
+
emma = await TestNode.create({location, events: {}, label: ' emma', authorIdentity: '{"kind":"author","pubkey":"ddd028fe0436ce4d1eaf697b3be3971ff009fabcd4949028adf8305d5291e1f7","privkey":"MC4CAQAwBQYDK2VwBCIEIO0eoNtBvNuA84mFckAIR0ozuwFbMwJhR+FbMOq+ZALf","createdAt":1782509271005}'});
|
|
234
|
+
|
|
235
|
+
await emma.subscribe({expectedCount: 1});
|
|
236
|
+
await Promise.all([alice.ready, bob.ready, carol.ready, david.ready, emma.ready]);
|
|
237
|
+
}, connectAllowanceMS + deliveryAllowanceMS);
|
|
238
|
+
it("alice receives kill", function () { expect(alice.events.kill).toEqual([null]); });
|
|
239
|
+
it(" bob receives kill", function () { expect( bob.events.kill).toEqual([null]); });
|
|
240
|
+
it("carol receives kill", function () { expect(carol.events.kill).toEqual([null]); });
|
|
241
|
+
it("david receives kill", function () { expect(david.events.kill).toEqual([null]); });
|
|
242
|
+
it(" emma only receives unkilled pub from bob", function () { expect( emma.events.kill).toEqual(inOrder.slice(1)); });
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
afterAll(async function () {
|
|
246
|
+
await alice.disconnect();
|
|
247
|
+
await bob.disconnect();
|
|
248
|
+
await carol?.disconnect();
|
|
249
|
+
await david?.disconnect();
|
|
250
|
+
await emma?.disconnect();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TODO:
|
|
3
|
+
[ ] Put agents in different regions.
|
|
4
|
+
|
|
5
|
+
instances with identities Alice, Bob, Carol, David, and Emma.
|
|
6
|
+
[ ] restarts to same identity
|
|
7
|
+
Alice and Bob will publish and subscribe. Both publish a series of handle/avatars, but Bob's user-data history ends with none (a pullback).
|
|
8
|
+
Carol will subscribe (with non-canonical hashtag differences) before Alice publishes
|
|
9
|
+
David will subscribe after Alice publishes
|
|
10
|
+
Emma will subscribes after Alice deletes the alert, and receives nothing
|
|
11
|
+
|
|
12
|
+
Alice publishes alert
|
|
13
|
+
Bob publishes text reply to Alice's alert
|
|
14
|
+
Alice and Bob publish image reply to Alice's alert
|
|
15
|
+
Alice publishes video reply to Alice's alert
|
|
16
|
+
Alice and Bob change handle: Alice1, Alice2, Alice3, Alice(none), Alice4; Bob1, Bob2, Bob3, Bob(none and stops)
|
|
17
|
+
Alice and Bob change avatar: similarly
|
|
18
|
+
Carol has private handle and avatar for Alice: each time rotates through Alice1, Alice2, Alice(none), Alice3
|
|
19
|
+
Alice deletes image reply.
|
|
20
|
+
[ ] Alice receives immediately
|
|
21
|
+
[ ] Carol receives immediately
|
|
22
|
+
[ ] Alice receives after restart
|
|
23
|
+
[ ] Carol receives after restart
|
|
24
|
+
[ ] David receives on start
|
|
25
|
+
[ ] Emma receives nothing on startup after Alice had deleted alert.
|
|
26
|
+
|
|
27
|
+
On receiving:
|
|
28
|
+
[ ] Alert has hashtag
|
|
29
|
+
[ ] subscribe ignores non-canonical differences: leading emoji, case, spacing, diacriticals
|
|
30
|
+
[ ] if subscriber does not start with emoji, it picks up leading emoji from first it sees and is retained for next session
|
|
31
|
+
[ ] Alert, reply have sender
|
|
32
|
+
[ ] Alert, reply have timestamp and compute remaining time
|
|
33
|
+
[ ] Replies can have one attachmment with mime type
|
|
34
|
+
[ ] Images are downsampled so that largest dimension is no larger than 1024.
|
|
35
|
+
[ ] Avatar is data URL
|
|
36
|
+
[ ] Avatar is downsampled so that largest dimension is no larger than 128.
|
|
37
|
+
[ ] Composite handle is private || shared || 'anonymous'
|
|
38
|
+
[ ] Composite avatar is private || shared || identicon from identity tag
|
|
39
|
+
|
|
40
|
+
[ ] Throttled flood of 1000 alerts
|
|
41
|
+
*/
|
|
42
|
+
const { describe, it, expect, beforeAll, afterAll } = globalThis;
|
|
43
|
+
import { P2PWebNetwork } from '../public/javascripts/p2pWebNetwork.js';
|
|
44
|
+
import { location as region } from '../server/getLocation.js'; // First invocation caches.
|
|
45
|
+
|
|
46
|
+
describe("CivilDefense", function () {
|
|
47
|
+
let alice, bob, carol, david, emma;
|
|
48
|
+
beforeAll(async function () {
|
|
49
|
+
alice = await P2PWebNetwork.create({region});
|
|
50
|
+
bob = await P2PWebNetwork.create({region});
|
|
51
|
+
carol = await P2PWebNetwork.create({region});
|
|
52
|
+
});
|
|
53
|
+
afterAll(async function () {
|
|
54
|
+
await alice.disconnect();
|
|
55
|
+
await bob.disconnect();
|
|
56
|
+
await carol.disconnect();
|
|
57
|
+
});
|
|
58
|
+
it("fixme.", function () {
|
|
59
|
+
expect(1).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TODO:
|
|
3
|
+
- subscribe and publish to a series of owned/since:'latest' topic that includes a kill, but Bob's history ends with that kill.
|
|
4
|
+
- chunking
|
|
5
|
+
- Put agents in different regions.
|
|
6
|
+
- Throttled flood of 1000 alerts
|
|
7
|
+
*/
|
|
8
|
+
const { describe, it, expect, beforeAll, afterAll } = globalThis;
|
|
9
|
+
import { P2PWebNetwork as Node } from '../index.js';
|
|
10
|
+
|
|
11
|
+
describe("CivilDefense", function () {
|
|
12
|
+
// These two are not acceptance criteria for how long the operation should take, but are instead
|
|
13
|
+
// how long we are willing to wait in in the test before assuming it is never going to happen.
|
|
14
|
+
const connectAllowanceMS = 20e3;
|
|
15
|
+
const deliveryAllowanceMS = 10e3;
|
|
16
|
+
const location = {lat: 37.468640806567656, lng:-122.25797749532343};
|
|
17
|
+
Node.setSessionRegion(location); //fixme
|
|
18
|
+
|
|
19
|
+
let alice, bob, carol, david, emma;
|
|
20
|
+
let aliceKillTag, currentOperation;
|
|
21
|
+
|
|
22
|
+
const version = Date.now(); // Each run gets a new topic.
|
|
23
|
+
const eventName = `civilDefense.io:${version}:pubsub-test`;
|
|
24
|
+
const regionCode = Node.regionCode(location.lat, location.lng);
|
|
25
|
+
class TestNode extends Node { // Version of Node specific to these tests.
|
|
26
|
+
async publish({message, killTag, ...rest}) { // Log publish, and default eventName/region for these tests.
|
|
27
|
+
const published = await super.publish({eventName, region:regionCode, message, killTag, ...rest});
|
|
28
|
+
this.log(killTag ? 'kill' : 'publish', currentOperation, message || killTag);
|
|
29
|
+
return published;
|
|
30
|
+
}
|
|
31
|
+
resetExpectedPromise(expectedCount) {
|
|
32
|
+
// Sets a current internal handler, without updating subscription, which records handled messages in this.events[currentOperation].
|
|
33
|
+
// Returns a ready promise that resolves when the expectedCount has been received.
|
|
34
|
+
return this.ready = new Promise(resolve => {
|
|
35
|
+
const handlerTime = Date.now();
|
|
36
|
+
this.handler = ({message, receiver, ts:pubTime}) => { // Ensure that the receiver's events[currentOperation] is a list, and push message on to it.
|
|
37
|
+
// FIXME: ts is undefined for a kill, which is weird:
|
|
38
|
+
// 1. I would think that Axona needs the time in order to dedupe and order properly?
|
|
39
|
+
// 2. The app may need the time, especially since we are not reliably getting events in ts order. (See "wrong order" comment, below.)
|
|
40
|
+
pubTime ||= 0;
|
|
41
|
+
const start = Math.max(pubTime, handlerTime);
|
|
42
|
+
const elapsed = Date.now() - start;
|
|
43
|
+
const data = receiver.events[currentOperation] ||= [];
|
|
44
|
+
receiver.log(`received ${currentOperation} ${data.length} ${message} after ${elapsed.toLocaleString()} ms.`);
|
|
45
|
+
data.push(message || null);
|
|
46
|
+
if (data.length < expectedCount) return;
|
|
47
|
+
resolve();
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async subscribe({expectedCount = 0, handler = (...rest) => this.handler?.(...rest), ...rest} = {}) {
|
|
52
|
+
const ready = this.resetExpectedPromise(expectedCount);
|
|
53
|
+
const subscribed = await super.subscribe({eventName, region:regionCode, handler});
|
|
54
|
+
this.log('subscribed, expecting', expectedCount);
|
|
55
|
+
return subscribed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const inOrder = ['alice pub', ' bob pub'];
|
|
59
|
+
|
|
60
|
+
beforeAll(async function () {
|
|
61
|
+
[alice, bob] = await Promise.all([
|
|
62
|
+
TestNode.create({location, events: {}, label: 'alice', authorIdentity: '{"kind":"author","pubkey":"b08988518013fabc5949353281d263cb7916f273f3f8badf0b16443a67d0b05c","privkey":"MC4CAQAwBQYDK2VwBCIEIC7ZrU8C2WSBdoBQW/JoE0ZYRkp/kFcCGf7H25DVqM17","createdAt":1782508727637}'}),
|
|
63
|
+
TestNode.create({location, events: {}, label: ' bob', authorIdentity: '{"kind":"author","pubkey":"6447bae1e8f1d5c99243251b43b4d354fb4928c10b69076563617c06fff46ca3","privkey":"MC4CAQAwBQYDK2VwBCIEIGGHVqedJBPtzSDR3P03HHpWTPlyXteHJAPtABmdwujM","createdAt":1782508849433}'})
|
|
64
|
+
]);
|
|
65
|
+
await Promise.all([alice.subscribe({expectedCount: 2}), bob.subscribe({expectedCount: 2})]);
|
|
66
|
+
// Now carol joins and subscribes.
|
|
67
|
+
carol = await TestNode.create({location, events: {}, label: 'carol', authorIdentity: '{"kind":"author","pubkey":"9fac71086211be29c685fc8aeab7725f4e78a0476482b817b9160273a40812e8","privkey":"MC4CAQAwBQYDK2VwBCIEILSc0paO923M0X8d8ux6JkjEFz65gk2BVBPlTtlZD6Q3","createdAt":1782508860832}'});
|
|
68
|
+
await carol.subscribe({expectedCount: 2});
|
|
69
|
+
|
|
70
|
+
currentOperation = 'initial';
|
|
71
|
+
// 'alice pub' starts and completes before 'bob pub' starts.
|
|
72
|
+
aliceKillTag = await alice.publish({message: 'alice pub'});
|
|
73
|
+
await TestNode.delay(500); // FIXME: without this delay, subscription handlers are called in the wrong order.
|
|
74
|
+
await bob.publish({message: ' bob pub'});
|
|
75
|
+
await Promise.all([alice.ready, bob.ready, carol.ready]);
|
|
76
|
+
}, 2 * connectAllowanceMS + deliveryAllowanceMS);
|
|
77
|
+
describe("initial", function () {
|
|
78
|
+
it("alice receives all pubs from herself and bob", function () { expect(alice.events.initial).toEqual(inOrder); });
|
|
79
|
+
it(" bob receives all pubs from himself and bob", function () { expect( bob.events.initial).toEqual(inOrder); });
|
|
80
|
+
it("carol receives all pubs from alice and bob", function () { expect(carol.events.initial).toEqual(inOrder); });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("restart", function () {
|
|
84
|
+
beforeAll(async function () {
|
|
85
|
+
await bob.disconnect();
|
|
86
|
+
carol.handler = null; // We're not going to disconnect this instance of carol, but let's not get confused by subscription callbacks.
|
|
87
|
+
[bob, carol, david] = await Promise.all([
|
|
88
|
+
TestNode.create({location, events: bob.events, label: ' bob', authorIdentity: bob.authorIdentity}),
|
|
89
|
+
TestNode.create({location, events: carol.events, label: 'carol', authorIdentity: carol.authorIdentity}),
|
|
90
|
+
TestNode.create({location, events: {}, label: 'david', authorIdentity: '{"kind":"author","pubkey":"4071c632d8aedfeee7293c23a539b368211cf903722d12d521eeda226047e1ed","privkey":"MC4CAQAwBQYDK2VwBCIEIN/rAuWAlhP2blF3neaoXxLM727hQ1ZzR0hsg9yYS6hd","createdAt":1782508870567}'})
|
|
91
|
+
]);
|
|
92
|
+
currentOperation = 'restart';
|
|
93
|
+
await Promise.all([bob.subscribe({expectedCount: 2}), carol.subscribe({expectedCount: 2}), david.subscribe({expectedCount: 2})]);
|
|
94
|
+
await Promise.all([bob.ready, carol.ready, david.ready]);
|
|
95
|
+
}, 1 * connectAllowanceMS + deliveryAllowanceMS);
|
|
96
|
+
it(" bob receives all pubs from himself and bob", function () { expect( bob.events.restart).toEqual(inOrder); });
|
|
97
|
+
it("carol receives all pubs from alice and bob", function () { expect(carol.events.restart).toEqual(inOrder); });
|
|
98
|
+
it("david receives all pubs from alice and bob", function () { expect(david.events.restart).toEqual(inOrder); });
|
|
99
|
+
|
|
100
|
+
describe("after kill", function () {
|
|
101
|
+
beforeAll(async function () {
|
|
102
|
+
currentOperation = 'kill';
|
|
103
|
+
alice.resetExpectedPromise(1);
|
|
104
|
+
bob.resetExpectedPromise(1);
|
|
105
|
+
carol.resetExpectedPromise(1);
|
|
106
|
+
david.resetExpectedPromise(1);
|
|
107
|
+
await alice.publish({killTag: aliceKillTag});
|
|
108
|
+
await TestNode.delay(500); // FIXME: without this delay, someone already subscribed (like Bob) sometimes (rarely) doesn't get the kill callback.
|
|
109
|
+
emma = await TestNode.create({location, events: {}, label: ' emma', authorIdentity: '{"kind":"author","pubkey":"ddd028fe0436ce4d1eaf697b3be3971ff009fabcd4949028adf8305d5291e1f7","privkey":"MC4CAQAwBQYDK2VwBCIEIO0eoNtBvNuA84mFckAIR0ozuwFbMwJhR+FbMOq+ZALf","createdAt":1782509271005}'});
|
|
110
|
+
emma.subscribe({expectedCount: 1});
|
|
111
|
+
await Promise.all([alice.ready, bob.ready, carol.ready, david.ready, emma.ready]);
|
|
112
|
+
}, connectAllowanceMS + deliveryAllowanceMS);
|
|
113
|
+
it("alice receives kill", function () { expect(alice.events.kill).toEqual([null]); });
|
|
114
|
+
it(" bob receives kill", function () { expect( bob.events.kill).toEqual([null]); });
|
|
115
|
+
it("carol receives kill", function () { expect(carol.events.kill).toEqual([null]); });
|
|
116
|
+
it("david receives kill", function () { expect(david.events.kill).toEqual([null]); });
|
|
117
|
+
it(" emma only receives unkilled pub from bob", function () { expect( emma.events.kill).toEqual(inOrder.slice(1)); });
|
|
118
|
+
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
afterAll(async function () {
|
|
122
|
+
await alice.disconnect();
|
|
123
|
+
await bob.disconnect();
|
|
124
|
+
await carol?.disconnect();
|
|
125
|
+
await david?.disconnect();
|
|
126
|
+
await emma?.disconnect();
|
|
127
|
+
});
|
|
128
|
+
});
|