@yz-social/civildefense.io 4.4.4 → 4.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/public/javascripts/agent.js +2 -2
- package/public/javascripts/alert.js +557 -0
- package/public/javascripts/conversation.js +99 -0
- package/public/javascripts/hashtags.js +10 -9
- package/public/javascripts/main.js +4 -5
- package/public/javascripts/map.js +9 -573
- package/public/javascripts/p2pWebNetwork.js +17 -14
- package/public/javascripts/service-manager.js +1 -1
- package/public/javascripts/versions.js +1 -1
- package/public/service-worker.js +7 -4
- package/server/app.js +8 -7
- package/server/getLocation.js +1 -1
- package/server/location.json +2 -2
- package/spec/axonSpec.js +5 -11
- package/spec/conversationSpec.js +116 -0
- package/movie/camera.jpg +0 -0
- package/movie/movie.js +0 -151
- package/movie/script.js +0 -272
- package/movie/test.js +0 -2
- package/nginx/nginx.conf +0 -83
- package/nginx/yz.social +0 -81
- package/public/about/In case of Nazis, use CivilDefense.io.png +0 -0
- package/server/bridge.js +0 -397
- package/server/identity.js +0 -111
- package/spec/axonSpec.gratuitousNameChangeForSignal +0 -246
- package/spec/axonSpec.jsRemoveThePartAfterJS +0 -252
- package/spec/civildefenseSpec.js +0 -61
- package/spec/pubsubSpec.js +0 -128
|
@@ -2,7 +2,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
2
2
|
import { createNodeIdentity, createAuthorIdentity, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION } from '@axona/protocol';
|
|
3
3
|
import { stringToBytes, bytesToString, publishChunkedBytes, receiveChunkedBytes } from '@axona/protocol/std';
|
|
4
4
|
import { connect } from '@axona/protocol/connect.js';
|
|
5
|
-
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
|
|
6
5
|
const { BigInt, URL, File, pica } = globalThis;
|
|
7
6
|
|
|
8
7
|
/* Example:
|
|
@@ -20,21 +19,24 @@ export class P2PWebNetwork {
|
|
|
20
19
|
static setSessionRegion = resolveSessionRegion;
|
|
21
20
|
static sessionRegion = sessionRegionPromise;
|
|
22
21
|
static async create({infoLogger = console.log, debugLogger,
|
|
23
|
-
region = this.sessionRegion,
|
|
22
|
+
region = this.sessionRegion,
|
|
23
|
+
bridgeUrl = globalThis.process?.env.BRIDGE_URL || 'wss://bridge.axona.net',
|
|
24
24
|
} = {}) {
|
|
25
25
|
// Promise a ready-to-use network peer.
|
|
26
|
-
|
|
26
|
+
region = await region;
|
|
27
|
+
|
|
28
|
+
const { peer, nodeIdentity, transport, status, disconnect } = await connect({
|
|
27
29
|
bridge: bridgeUrl,
|
|
28
|
-
location:
|
|
30
|
+
location: region,
|
|
29
31
|
author: false
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
const network = new this();
|
|
33
|
-
Object.assign(network, {infoLogger, debugLogger,
|
|
35
|
+
Object.assign(network, {infoLogger, debugLogger, disconnector: disconnect, transport, nodeIdentity, peer});
|
|
34
36
|
network.resetStatePromises();
|
|
35
37
|
network.info(`Created network node for kernel ${this.kernelVersion} region 0x${this.regionCode(region.lat, region.lng).toString(16)}.`);
|
|
36
38
|
const { peers, ms } = status;
|
|
37
|
-
network.info(`Connected ${peers} connections in ${ms.toLocaleString()} ms.`);
|
|
39
|
+
network.info(`Connected ${peers} connections through ${bridgeUrl} in ${ms.toLocaleString()} ms.`);
|
|
38
40
|
network.attached(network);
|
|
39
41
|
return network;
|
|
40
42
|
}
|
|
@@ -161,10 +163,10 @@ export class P2PWebNetwork {
|
|
|
161
163
|
const {message, deleted, msgId, signerPubkey, topic, ts} = envelope;
|
|
162
164
|
this.debug('received', {msgId, topic, ts, signerPubkey, deleted, message});
|
|
163
165
|
if (deleted) {
|
|
164
|
-
handler({
|
|
166
|
+
handler({tag: msgId, payload: null, agent: signerPubkey, topic, ts}); // fixme remove topic, ts here and below.
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
167
|
-
handler({...message, agent: signerPubkey,
|
|
169
|
+
handler({...message, agent: signerPubkey, tag: msgId, topic, ts});
|
|
168
170
|
};
|
|
169
171
|
await this.peer.sub(topic, callback, {since});
|
|
170
172
|
} else {
|
|
@@ -172,17 +174,18 @@ export class P2PWebNetwork {
|
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
static currentPublishIdentity = null;
|
|
175
|
-
async publish({eventName, region, owner, signWith = this.constructor.currentPublishIdentity, issuedTime = Date.now(),
|
|
177
|
+
async publish({eventName, region, owner, signWith = this.constructor.currentPublishIdentity, issuedTime = Date.now(), killTag, payload, ...rest}) {
|
|
176
178
|
// Publish data to subscribers of eventName.
|
|
179
|
+
if (killTag && payload) throw new Error(`Specify killTag (${killTag}) or payload ($(JSON.stringify(payload)}), but not both.`);
|
|
177
180
|
await this.attachment; // Get connected.
|
|
178
181
|
const topic = {region, name: eventName};
|
|
179
182
|
if (owner) topic.owner = owner;
|
|
180
183
|
const options = {signWith};
|
|
181
|
-
this.debug('published', {topic,
|
|
184
|
+
this.debug('published', {topic, killTag, payload, issuedTime, rest, signWith});
|
|
182
185
|
if (payload) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
|
|
183
186
|
// 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.
|
|
184
|
-
if (!
|
|
185
|
-
return await this.peer.kill(topic,
|
|
187
|
+
if (!killTag) return await this.peer.pub(topic, {issuedTime, payload, ...rest}, options);
|
|
188
|
+
return await this.peer.kill(topic, killTag, options);
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
host() {
|
|
@@ -215,10 +218,10 @@ export class P2PWebNetwork {
|
|
|
215
218
|
}
|
|
216
219
|
// Todo: Integrate with AxonaPeer's complex logging.
|
|
217
220
|
debug(...rest) { // Add debug logspam.
|
|
218
|
-
this.debugLogger?.(this.
|
|
221
|
+
this.debugLogger?.(this.nodeIdentity.id, ...rest);
|
|
219
222
|
}
|
|
220
223
|
info(...rest) { // Add debug logspam.
|
|
221
|
-
(this.infoLogger || this.debugLogger)?.(this.
|
|
224
|
+
(this.infoLogger || this.debugLogger)?.(this.nodeIdentity.id, ...rest);
|
|
222
225
|
}
|
|
223
226
|
}
|
|
224
227
|
export default P2PWebNetwork;
|
|
@@ -2,7 +2,7 @@ const { Request, Response, URL, localStorage, BroadcastChannel } = globalThis;
|
|
|
2
2
|
import { appVersion } from './versions.js';
|
|
3
3
|
import { resetInactivityTimer, clickTip } from './main.js';
|
|
4
4
|
import { openDisplay } from './display.js';
|
|
5
|
-
import { go } from './
|
|
5
|
+
import { go } from './alert.js';
|
|
6
6
|
import { Int } from './translations.js';
|
|
7
7
|
|
|
8
8
|
/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pkg from '../package.json' with {type: 'json'};
|
|
2
2
|
export const appVersion = pkg.version; // Overall semver of app. Used in display, and for comparison by service worker.
|
|
3
|
-
export const dataVersion = appVersion.split('.')[0]; // Compatability differentiator used below.
|
|
3
|
+
export const dataVersion = globalThis.process?.env.EVENT_VERSION || appVersion.split('.')[0]; // Compatability differentiator used below.
|
|
4
4
|
|
|
5
5
|
export function stripLeadingEmoji(string) { // Return string without any leading emoji (which might be of varying
|
|
6
6
|
// length) followed by an optional emoji break character and any whitespace.
|
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.
|
|
3
|
+
const serviceVersion = '4.5.4';
|
|
4
4
|
|
|
5
5
|
async function cacheFirst({request, event}) {
|
|
6
6
|
// Handle request from any cache, else fetch and store it in serviceCache.
|
|
@@ -82,7 +82,9 @@ async function cacheSource(version, event) { // Cache source in the given versio
|
|
|
82
82
|
"javascripts/versions.js",
|
|
83
83
|
"javascripts/main.js",
|
|
84
84
|
"javascripts/display.js",
|
|
85
|
-
"javascripts/
|
|
85
|
+
"javascripts/conversation.js",
|
|
86
|
+
"javascripts/alert.js",
|
|
87
|
+
"javascripts/map.js",
|
|
86
88
|
"javascripts/hashtags.js",
|
|
87
89
|
"javascripts/s2.js",
|
|
88
90
|
"javascripts/agent.js",
|
|
@@ -163,6 +165,7 @@ async function cacheSource(version, event) { // Cache source in the given versio
|
|
|
163
165
|
|
|
164
166
|
// TODO: the libraries
|
|
165
167
|
].map(name => new Request(name, {cache: 'no-store'}))); // Might not be necessary, but if any browsers insist on their own caching...
|
|
168
|
+
|
|
166
169
|
await Promise.all([
|
|
167
170
|
// These are referenced within material web, but missing. Turns out we don't need them,
|
|
168
171
|
// but let's cache empty responses to keep the console cleaner.
|
|
@@ -205,10 +208,10 @@ self.addEventListener('notificationclick', event => {
|
|
|
205
208
|
clients
|
|
206
209
|
.matchAll({type: 'window', includeUncontrolled: true})
|
|
207
210
|
.then(async clientList => {
|
|
208
|
-
console.log('notification', {title, body, data, clientList});
|
|
211
|
+
console.log('notification', {title, body, tag, data, clientList});
|
|
209
212
|
for (const client of clientList) {
|
|
210
213
|
console.log('notification click found client');
|
|
211
|
-
return client.focus().then(() => client.postMessage({method: 'go', params: {
|
|
214
|
+
return client.focus().then(() => client.postMessage({method: 'go', params: {alert: tag, ...data}}));
|
|
212
215
|
}
|
|
213
216
|
// Client has been closed. Open one.
|
|
214
217
|
console.log('notification click opening client', data.url);
|
package/server/app.js
CHANGED
|
@@ -18,7 +18,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
18
18
|
.option('nPortals', {
|
|
19
19
|
alias: 'p',
|
|
20
20
|
type: 'number',
|
|
21
|
-
default: logicalCores,
|
|
21
|
+
default: Math.min(logicalCores, 5),
|
|
22
22
|
description: "The number of steady nodes that handle initial connections."
|
|
23
23
|
})
|
|
24
24
|
.option('baseURL', {
|
|
@@ -82,7 +82,6 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
82
82
|
// expressWs(app);
|
|
83
83
|
// const Yz = await import('./routes/index.js');
|
|
84
84
|
|
|
85
|
-
//console.log(`${cpus()[0].model}, ${logicalCores} logical cores. Starting ${argv.nPortals}.`);
|
|
86
85
|
app.use(express.json());
|
|
87
86
|
|
|
88
87
|
app.use('/images', express.static(resolve('../public/images'), {
|
|
@@ -96,17 +95,19 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
|
|
|
96
95
|
|
|
97
96
|
app.listen(port);
|
|
98
97
|
console.log('Listening on', port, 'and starting', argv.nPortals, 'nodes.');
|
|
99
|
-
for (let i = 0; i < argv.nPortals; i++)
|
|
98
|
+
for (let i = 0; i < argv.nPortals; i++) {
|
|
99
|
+
cluster.fork();
|
|
100
|
+
await new Promise(resolve => setTimeout(resolve, 1e3));
|
|
101
|
+
}
|
|
100
102
|
} else {
|
|
101
103
|
process.title = 'axona-starting';
|
|
102
104
|
const { P2PWebNetwork, location } = await import('../index.js');
|
|
103
|
-
await P2PWebNetwork.delay(cluster.worker?.id * 1e3); // One second between startups.
|
|
104
105
|
const network = await P2PWebNetwork.create({region: location});
|
|
105
|
-
process.title = 'axona-' + network.
|
|
106
|
-
let update =
|
|
106
|
+
process.title = 'axona-' + network.nodeIdentity.id;
|
|
107
|
+
//let update = setInterval(() => network.info(network.peer.health().axonRoles.length, 'axons'), 10e3);
|
|
107
108
|
process.on('SIGINT', async () => { // Leave the network politely.
|
|
108
109
|
console.log(process.title, 'Shutdown for Ctrl+C');
|
|
109
|
-
clearInterval(update)
|
|
110
|
+
//clearInterval(update)
|
|
110
111
|
await network.disconnect();
|
|
111
112
|
process.exit(0);
|
|
112
113
|
});
|
package/server/getLocation.js
CHANGED
|
@@ -14,6 +14,6 @@ export const data = await import(filename, {with: { type: 'json' }})
|
|
|
14
14
|
await fs.writeFile(resolve(filename), string, 'utf8');
|
|
15
15
|
return {default: JSON.parse(string)};
|
|
16
16
|
});
|
|
17
|
-
export const [lat, lng] = data.default.loc.split(',').map(parseFloat);
|
|
17
|
+
export const [lat, lng] = (globalThis.process?.env.LAT_LNG || data.default.loc).split(',').map(parseFloat);
|
|
18
18
|
export const location = {lat, lng};
|
|
19
19
|
|
package/server/location.json
CHANGED
package/spec/axonSpec.js
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
/*
|
|
2
2
|
FIXME: Things that either don't pass, or require undocumented workarounds.
|
|
3
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
4
|
|
|
8
5
|
To RUN, e.g., in NodeJS:
|
|
9
6
|
- 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
|
|
7
|
+
- To switch between them, don't forget to change the wss url a few lines down from here.
|
|
11
8
|
- Have jasmine or the like installed and initialized, and then e.g., npx jasmine spec/axonSpec.js.
|
|
12
9
|
|
|
13
10
|
It is worth running this several times. It sometimes works once, and then fails or has enormous connect times on another run.
|
|
14
11
|
|
|
15
12
|
The logging tells the story.
|
|
16
|
-
Alice, Bob, and
|
|
13
|
+
Alice, Bob, Carol, David, and Emma are Node instances. (Defined below, followed by the Jasmine test suite.)
|
|
17
14
|
Alice and Bob will subscribe and publish to an open/since:'all' topic.
|
|
18
15
|
Carol will join and subscribe between the previous subscriptions and their publications.
|
|
19
16
|
After publications, Bob will politely disconnect, and then restart and subscribe again to get same results.
|
|
20
17
|
Carol will restart without an explicit disconnect, and subscribe again after publications.
|
|
21
18
|
David will join and subscribe after publications.
|
|
19
|
+
Alice kills her publication and Emma joins.
|
|
22
20
|
*/
|
|
23
21
|
const { describe, it, expect, beforeAll, afterAll, BigInt } = globalThis;
|
|
24
22
|
import { createAuthorIdentity, regionCenter, geoCellId, geoCellCenter, WIRE_VERSION, KERNEL_VERSION, deriveTopicId, metricTopic } from '@axona/protocol';
|
|
25
23
|
import { connect } from '@axona/protocol/connect.js';
|
|
26
24
|
globalThis.RTCPeerConnection ||= await import('node-datachannel/polyfill').then(ndc => ndc.RTCPeerConnection);
|
|
27
25
|
|
|
28
|
-
class Node { // Stuff we have to do every time.
|
|
26
|
+
class Node { // Stuff we have to do every time.
|
|
29
27
|
static version = KERNEL_VERSION;
|
|
30
28
|
log(...rest) {
|
|
31
29
|
console.log(new Date(), this.label, this.nodeIdentity.id.slice(0, 10), ...rest);
|
|
@@ -132,10 +130,6 @@ describe("CivilDefense", function () {
|
|
|
132
130
|
return this.ready = new Promise(resolve => {
|
|
133
131
|
const handlerTime = Date.now();
|
|
134
132
|
this.handler = ({message, receiver, ts:pubTime}) => { // Ensure that the receiver's events[currentOperation] is a list, and push message on to it.
|
|
135
|
-
// FIXME: ts is undefined for a kill, which is weird:
|
|
136
|
-
// 1. I would think that Axona needs the time in order to dedupe and order properly?
|
|
137
|
-
// 2. The app may need the time, especially since we are not reliably getting events in ts order. (See "wrong order" comment, below.)
|
|
138
|
-
pubTime ||= 0;
|
|
139
133
|
const start = Math.max(pubTime, handlerTime);
|
|
140
134
|
const elapsed = Date.now() - start;
|
|
141
135
|
const data = receiver.events[currentOperation] ||= [];
|
|
@@ -175,8 +169,8 @@ describe("CivilDefense", function () {
|
|
|
175
169
|
currentOperation = 'initial';
|
|
176
170
|
// 'alice pub' starts and completes before 'bob pub' starts.
|
|
177
171
|
aliceKillTag = await alice.publish({message: 'alice pub'});
|
|
178
|
-
await TestNode.delay(500); // FIXME: without this delay, subscription handlers are called in the wrong order.
|
|
179
172
|
console.log('alice published');
|
|
173
|
+
await TestNode.delay(500); // FIXME: without this delay, subscription handlers are called in the wrong order.
|
|
180
174
|
await bob.publish({message: ' bob pub'});
|
|
181
175
|
console.log('bob published');
|
|
182
176
|
//alice.subscribeOpenMetrics({eventName, region:regionCode, handler: envelope => console.log('*** fixme got metrics', envelope)});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { describe, it, expect, beforeAll, afterAll, BigInt } = globalThis;
|
|
2
|
+
import { Conversation } from '../public/javascripts/conversation.js';
|
|
3
|
+
|
|
4
|
+
describe("Conversation", function () {
|
|
5
|
+
let agent;
|
|
6
|
+
let conversation;
|
|
7
|
+
const tag = '123';
|
|
8
|
+
const payload = 'cake';
|
|
9
|
+
beforeAll(function () {
|
|
10
|
+
agent = {handle: 'alice'};
|
|
11
|
+
conversation = Conversation.ensure({tag, payload, agent});
|
|
12
|
+
});
|
|
13
|
+
describe("creation", function () {
|
|
14
|
+
it("initializes properties.", function () {
|
|
15
|
+
expect(conversation.agent).toBe(agent);
|
|
16
|
+
expect(conversation.payload).toBe(payload);
|
|
17
|
+
});
|
|
18
|
+
it("remembers conversations with the same tag.", function () {
|
|
19
|
+
expect(Conversation.ensure({tag, payload, agent})).toBe(conversation);
|
|
20
|
+
});
|
|
21
|
+
it("Properties can be ommitted for existing tag.", function () {
|
|
22
|
+
expect(Conversation.ensure({tag, payload})).toBe(conversation);
|
|
23
|
+
});
|
|
24
|
+
it("rejects changes by default.", function () {
|
|
25
|
+
expect(() => Conversation.ensure({tag, payload: 'other', agent})).toThrow();
|
|
26
|
+
});
|
|
27
|
+
describe("caching", function () {
|
|
28
|
+
let keep = true;
|
|
29
|
+
let tag = "caching";
|
|
30
|
+
class CacheConversation extends Conversation {
|
|
31
|
+
update() { return keep && this; }
|
|
32
|
+
initialize() { return keep && this; }
|
|
33
|
+
}
|
|
34
|
+
it("ends with explicit removal.", function () {
|
|
35
|
+
let initial = Conversation.ensure({tag, agent, payload});
|
|
36
|
+
expect(initial).toBeTruthy();
|
|
37
|
+
expect(Conversation.getItem(tag)).toBe(initial);
|
|
38
|
+
expect(Conversation.removeItem(tag)).toBe(initial);
|
|
39
|
+
expect(Conversation.getItem(tag)).toBeFalsy();
|
|
40
|
+
});
|
|
41
|
+
describe("deleting data", function () {
|
|
42
|
+
it("keeps new if not deleting.", function () {
|
|
43
|
+
tag = 'keep';
|
|
44
|
+
let initial = Conversation.ensure({tag, agent, payload});
|
|
45
|
+
expect(initial).toBeTruthy();
|
|
46
|
+
expect(Conversation.ensure({tag, agent, payload})).toBe(initial);
|
|
47
|
+
expect(Conversation.getItem(tag)).toBe(initial);
|
|
48
|
+
});
|
|
49
|
+
it("is skipped if delete data.", function () {
|
|
50
|
+
tag = 'skip';
|
|
51
|
+
let initial = Conversation.ensure({tag, agent});
|
|
52
|
+
expect(initial).toBeFalsy();
|
|
53
|
+
expect(Conversation.getItem(tag)).toBeFalsy();
|
|
54
|
+
});
|
|
55
|
+
it("is removed if delete data.", function () {
|
|
56
|
+
tag = 'skip';
|
|
57
|
+
let initial = Conversation.ensure({tag, agent, payload});
|
|
58
|
+
expect(initial).toBeTruthy();
|
|
59
|
+
expect(Conversation.getItem(tag)).toBeTruthy();
|
|
60
|
+
Conversation.ensure({tag, agent});
|
|
61
|
+
expect(Conversation.getItem(tag)).toBeFalsy();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("null/item caching convention", function () {
|
|
65
|
+
// Do we really need/want this convetion?
|
|
66
|
+
it("keeps if initialize answers conversation.", function () {
|
|
67
|
+
keep = true;
|
|
68
|
+
tag = 'keepA';
|
|
69
|
+
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
70
|
+
expect(initial).toBeTruthy();
|
|
71
|
+
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
72
|
+
expect(CacheConversation.getItem(tag)).toBe(initial);
|
|
73
|
+
});
|
|
74
|
+
it("is skipped if initialize answers falsy.", function () {
|
|
75
|
+
keep = false;
|
|
76
|
+
tag = 'skipA';
|
|
77
|
+
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
78
|
+
expect(initial).toBeFalsy();
|
|
79
|
+
expect(CacheConversation.getItem(tag)).toBeFalsy();
|
|
80
|
+
});
|
|
81
|
+
it("keeps existing if update answers conversation.", function () {
|
|
82
|
+
keep = true;
|
|
83
|
+
tag = 'keepB';
|
|
84
|
+
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
85
|
+
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
86
|
+
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
87
|
+
expect(CacheConversation.getItem(tag)).toBe(initial);
|
|
88
|
+
});
|
|
89
|
+
it("destroys existing if update answers falsy.", function () {
|
|
90
|
+
keep = true;
|
|
91
|
+
tag = 'skipB';
|
|
92
|
+
let initial = CacheConversation.ensure({tag, agent, payload});
|
|
93
|
+
expect(CacheConversation.ensure({tag, agent, payload})).toBe(initial);
|
|
94
|
+
keep = false;
|
|
95
|
+
expect(CacheConversation.ensure({tag, agent, payload})).toBeFalsy();
|
|
96
|
+
expect(CacheConversation.getItem(tag)).toBeFalsy();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("replies", function () {
|
|
103
|
+
beforeAll(function () {
|
|
104
|
+
conversation.ensure({payload: "second", issuedTime: 3, tag: 'z'});
|
|
105
|
+
conversation.ensure({payload: "deleted", issuedTime: 2, tag: 'y'});
|
|
106
|
+
conversation.ensure({payload: "first", issuedTime: 1, tag: 'x'});
|
|
107
|
+
conversation.ensure({payload: null, issuedTime: 4, tag: 'y'});
|
|
108
|
+
});
|
|
109
|
+
it("adds replies in timestamp order.", function () {
|
|
110
|
+
expect(conversation.items.map(reply => reply.payload)).toEqual(["first", "second"]);
|
|
111
|
+
});
|
|
112
|
+
it("removes deleted replies.", function () { // fixme eachReply
|
|
113
|
+
expect(conversation.items.find(reply => !reply.payload)).toBeFalsy();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
package/movie/camera.jpg
DELETED
|
Binary file
|