@agnt-sdk/simon-server 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +317 -2
- package/dist/account.d.ts +59 -0
- package/dist/account.js +244 -0
- package/dist/auth.d.ts +42 -0
- package/dist/auth.js +96 -0
- package/dist/config.d.ts +219 -0
- package/dist/config.js +300 -0
- package/dist/context.d.ts +57 -0
- package/dist/context.js +2 -0
- package/dist/continuity.d.ts +35 -0
- package/dist/continuity.js +74 -0
- package/dist/deliver.d.ts +22 -0
- package/dist/deliver.js +208 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +11 -0
- package/dist/extensions.d.ts +216 -0
- package/dist/extensions.js +75 -0
- package/dist/fanout.d.ts +32 -0
- package/dist/fanout.js +79 -0
- package/dist/http.d.ts +69 -0
- package/dist/http.js +112 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +12 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +41 -0
- package/dist/net/dns.d.ts +31 -0
- package/dist/net/dns.js +100 -0
- package/dist/net/fetch.d.ts +48 -0
- package/dist/net/fetch.js +173 -0
- package/dist/net/ip.d.ts +38 -0
- package/dist/net/ip.js +159 -0
- package/dist/net/psl.d.ts +22 -0
- package/dist/net/psl.js +91 -0
- package/dist/net/public-suffixes.d.ts +10 -0
- package/dist/net/public-suffixes.js +6885 -0
- package/dist/notice.d.ts +5 -0
- package/dist/notice.js +61 -0
- package/dist/outbox.d.ts +14 -0
- package/dist/outbox.js +356 -0
- package/dist/parts.d.ts +29 -0
- package/dist/parts.js +443 -0
- package/dist/public.d.ts +43 -0
- package/dist/public.js +134 -0
- package/dist/published.d.ts +3 -0
- package/dist/published.js +140 -0
- package/dist/rate.d.ts +28 -0
- package/dist/rate.js +77 -0
- package/dist/readers.d.ts +31 -0
- package/dist/readers.js +48 -0
- package/dist/register.d.ts +4 -0
- package/dist/register.js +401 -0
- package/dist/resolver.d.ts +140 -0
- package/dist/resolver.js +386 -0
- package/dist/send.d.ts +30 -0
- package/dist/send.js +216 -0
- package/dist/server.d.ts +52 -0
- package/dist/server.js +234 -0
- package/dist/signal.d.ts +5 -0
- package/dist/signal.js +137 -0
- package/dist/store/contract.d.ts +4 -0
- package/dist/store/contract.js +1177 -0
- package/dist/store/sqlite-migration.d.ts +82 -0
- package/dist/store/sqlite-migration.js +1019 -0
- package/dist/store/sqlite.d.ts +108 -0
- package/dist/store/sqlite.js +992 -0
- package/dist/store/store.d.ts +400 -0
- package/dist/store/store.js +23 -0
- package/dist/sync.d.ts +19 -0
- package/dist/sync.js +199 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +87 -0
- package/dist/webhooks.d.ts +10 -0
- package/dist/webhooks.js +162 -0
- package/dist/worker.d.ts +15 -0
- package/dist/worker.js +33 -0
- package/package.json +57 -4
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Store contract as tests. Any store proves itself by running them:
|
|
3
|
+
*
|
|
4
|
+
* import { storeContract } from '@agnt-sdk/simon-server/contract';
|
|
5
|
+
* storeContract('my store', () => new MyStore());
|
|
6
|
+
*
|
|
7
|
+
* `make` returns a fresh, empty store each time. Every rule in store.ts that the core relies on is here.
|
|
8
|
+
*/
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
11
|
+
import { MAX_STORED_KEYS } from "./store.js";
|
|
12
|
+
const T0 = Date.parse('2026-10-15T00:00:00.000Z');
|
|
13
|
+
const at = (ms = 0) => new Date(T0 + ms).toISOString();
|
|
14
|
+
const HOUR = 3_600_000;
|
|
15
|
+
const sha = (c) => c.repeat(64);
|
|
16
|
+
const ids = (page) => page.messages.map(e => e.id);
|
|
17
|
+
const agent = (address, ...keys) => ({
|
|
18
|
+
address,
|
|
19
|
+
kind: 'solo',
|
|
20
|
+
principal: null,
|
|
21
|
+
keys: keys.map(key => ({ key, added: at(), revoked: null })),
|
|
22
|
+
mailbox: false,
|
|
23
|
+
createdAt: at(),
|
|
24
|
+
retiredAt: null,
|
|
25
|
+
});
|
|
26
|
+
let n = 0;
|
|
27
|
+
function message(from, to, extra = {}) {
|
|
28
|
+
const m = {
|
|
29
|
+
v: 1,
|
|
30
|
+
from,
|
|
31
|
+
to,
|
|
32
|
+
inReplyTo: null,
|
|
33
|
+
parts: [],
|
|
34
|
+
headers: {},
|
|
35
|
+
sentAt: at(),
|
|
36
|
+
signature: 'AA',
|
|
37
|
+
n: n++,
|
|
38
|
+
...extra,
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
id: String(n).padStart(64, '0'),
|
|
42
|
+
message: m,
|
|
43
|
+
carrier: 'https://here.test',
|
|
44
|
+
claims: [],
|
|
45
|
+
signedClaims: null,
|
|
46
|
+
receivedAt: at(),
|
|
47
|
+
expiresAt: at(24 * HOUR),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const delivery = (id, domain, extra = {}) => ({
|
|
51
|
+
id,
|
|
52
|
+
domain,
|
|
53
|
+
recipients: [`bob@${domain}`],
|
|
54
|
+
hints: {},
|
|
55
|
+
server: null,
|
|
56
|
+
status: 'pending',
|
|
57
|
+
reason: null,
|
|
58
|
+
attempts: 0,
|
|
59
|
+
nextAttemptAt: at(),
|
|
60
|
+
createdAt: at(),
|
|
61
|
+
deadline: at(120 * HOUR),
|
|
62
|
+
...extra,
|
|
63
|
+
});
|
|
64
|
+
const arrival = (id, recipient, extra = {}) => ({
|
|
65
|
+
id,
|
|
66
|
+
recipient,
|
|
67
|
+
hints: {},
|
|
68
|
+
attempts: 0,
|
|
69
|
+
dueAt: at(),
|
|
70
|
+
...extra,
|
|
71
|
+
});
|
|
72
|
+
const SELF = 'https://here.test';
|
|
73
|
+
const lease = (token, until = at(2 * 60_000)) => ({ token, until });
|
|
74
|
+
export function storeContract(name, make) {
|
|
75
|
+
describe(`Store contract: ${name}`, () => {
|
|
76
|
+
let store;
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
store = await make();
|
|
79
|
+
});
|
|
80
|
+
afterEach(() => store.close());
|
|
81
|
+
describe('agents', () => {
|
|
82
|
+
it('gives an address and a key to one agent, forever', async () => {
|
|
83
|
+
assert.equal(await store.createAgent(agent('ada@a.test', 'k1')), 'created');
|
|
84
|
+
assert.equal(await store.createAgent(agent('ada@a.test', 'k2')), 'address_taken');
|
|
85
|
+
assert.equal(await store.createAgent(agent('bob@a.test', 'k1')), 'key_in_use');
|
|
86
|
+
assert.equal((await store.agentByKey('k1'))?.address, 'ada@a.test');
|
|
87
|
+
assert.equal(await store.agentByAddress('bob@a.test'), null);
|
|
88
|
+
await store.retireAgent('ada@a.test', at(1));
|
|
89
|
+
const retired = await store.agentByAddress('ada@a.test');
|
|
90
|
+
assert.equal(retired?.retiredAt, at(1));
|
|
91
|
+
assert.equal(retired?.keys[0].revoked, at(1));
|
|
92
|
+
assert.equal(retired?.unpairing ?? null, null);
|
|
93
|
+
assert.equal(await store.createAgent(agent('ada@a.test', 'k3')), 'address_taken');
|
|
94
|
+
});
|
|
95
|
+
it('keeps the unpairing that retired an address, and only the first retirement', async () => {
|
|
96
|
+
await store.createAgent(agent('ada@a.test', 'k1'));
|
|
97
|
+
await store.retireAgent('ada@a.test', at(1), 'UNPAIRING');
|
|
98
|
+
await store.retireAgent('ada@a.test', at(2), 'OTHER');
|
|
99
|
+
await store.retireAgent('ada@a.test', at(3));
|
|
100
|
+
const retired = await store.agentByAddress('ada@a.test');
|
|
101
|
+
assert.deepEqual([retired?.retiredAt, retired?.unpairing, retired?.keys[0].revoked], [at(1), 'UNPAIRING', at(1)]);
|
|
102
|
+
// Racing retirements: one wins, whole.
|
|
103
|
+
await store.createAgent(agent('bob@a.test', 'k2'));
|
|
104
|
+
await Promise.all([at(4), at(5), at(6)].map((t, i) => store.retireAgent('bob@a.test', t, `U${i}`)));
|
|
105
|
+
const bob = await store.agentByAddress('bob@a.test');
|
|
106
|
+
assert.equal(bob?.keys[0].revoked, bob?.retiredAt);
|
|
107
|
+
assert.equal(bob?.unpairing, `U${[at(4), at(5), at(6)].indexOf(bob.retiredAt)}`);
|
|
108
|
+
await store.retireAgent('nobody@a.test', at(1));
|
|
109
|
+
assert.equal(await store.agentByAddress('nobody@a.test'), null);
|
|
110
|
+
});
|
|
111
|
+
it('caps active keys atomically, even when adds race', async () => {
|
|
112
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
113
|
+
const results = await Promise.all(['k1', 'k2', 'k3', 'k4'].map(key => store.addKey('ada@a.test', { key, added: at(), revoked: null }, 3)));
|
|
114
|
+
assert.equal(results.filter(r => r === 'added').length, 2);
|
|
115
|
+
assert.equal(results.filter(r => r === 'too_many_keys').length, 2);
|
|
116
|
+
assert.equal(await store.addKey('ada@a.test', { key: 'k0', added: at(), revoked: null }, 9), 'key_in_use');
|
|
117
|
+
assert.deepEqual((await store.agentByAddress('ada@a.test')).keys[0].key, 'k0');
|
|
118
|
+
});
|
|
119
|
+
it('checks final document admission inside concurrent additions and recovery, with no partial mutation', async () => {
|
|
120
|
+
assert.equal(await store.createAgent(agent('ada@a.test', 'k0'), () => false), 'too_many_keys');
|
|
121
|
+
assert.equal(await store.agentByAddress('ada@a.test'), null);
|
|
122
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
123
|
+
const results = await Promise.all(['k1', 'k2'].map(key => store.addKey('ada@a.test', { key, added: at(), revoked: null }, 32, a => a.keys.filter(k => k.revoked === null).length <= 2)));
|
|
124
|
+
assert.deepEqual(results.sort(), ['added', 'too_many_keys']);
|
|
125
|
+
const before = await store.agentByAddress('ada@a.test');
|
|
126
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'recovery', added: at(2), revoked: null }, () => false), 'too_many_keys');
|
|
127
|
+
assert.deepEqual(await store.agentByAddress('ada@a.test'), before);
|
|
128
|
+
assert.equal(await store.agentByKey('recovery'), null);
|
|
129
|
+
});
|
|
130
|
+
it("keeps a key's label, and a key without one has none", async () => {
|
|
131
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
132
|
+
await store.addKey('ada@a.test', { key: 'k1', added: at(), revoked: null, label: "Ada's phone" }, 9);
|
|
133
|
+
assert.deepEqual((await store.agentByAddress('ada@a.test')).keys, [
|
|
134
|
+
{ key: 'k0', added: at(), revoked: null },
|
|
135
|
+
{ key: 'k1', added: at(), revoked: null, label: "Ada's phone" },
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
it('revokes one key at a time, never the last, even when revocations race', async () => {
|
|
139
|
+
await store.createAgent(agent('ada@a.test', 'k0', 'k1', 'k2'));
|
|
140
|
+
await store.createAgent(agent('bob@a.test', 'kb'));
|
|
141
|
+
assert.equal(await store.revokeKey('ada@a.test', 'k2', at(1)), 'revoked');
|
|
142
|
+
assert.equal(await store.revokeKey('ada@a.test', 'k2', at(2)), 'not_found', 'already revoked');
|
|
143
|
+
assert.equal(await store.revokeKey('ada@a.test', 'kb', at(2)), 'not_found', "another agent's");
|
|
144
|
+
const results = await Promise.all(['k0', 'k1'].map(key => store.revokeKey('ada@a.test', key, at(3))));
|
|
145
|
+
assert.deepEqual(results.toSorted(), ['last_key', 'revoked']);
|
|
146
|
+
const keys = (await store.agentByAddress('ada@a.test')).keys;
|
|
147
|
+
assert.equal(keys[2].revoked, at(1), 'a revocation never moves');
|
|
148
|
+
assert.equal(keys.filter(k => k.revoked === null).length, 1);
|
|
149
|
+
assert.equal(await store.revokeKey('bob@a.test', 'kb', at(4)), 'last_key');
|
|
150
|
+
});
|
|
151
|
+
it('replaces every key on recovery, keeping the old ones as revoked', async () => {
|
|
152
|
+
await store.createAgent(agent('ada@a.test', 'k0', 'k1'));
|
|
153
|
+
await store.createAgent(agent('bob@a.test', 'kb'));
|
|
154
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'kb', added: at(5), revoked: null }), 'key_in_use');
|
|
155
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'k2', added: at(5), revoked: null }), 'replaced');
|
|
156
|
+
const keys = (await store.agentByAddress('ada@a.test')).keys;
|
|
157
|
+
assert.deepEqual(keys.map(k => [k.key, k.revoked]), [
|
|
158
|
+
['k0', at(5)],
|
|
159
|
+
['k1', at(5)],
|
|
160
|
+
['k2', null],
|
|
161
|
+
]);
|
|
162
|
+
assert.equal((await store.agentByKey('k0'))?.address, 'ada@a.test');
|
|
163
|
+
// A retired address never has a key again, whichever call reaches it first.
|
|
164
|
+
await store.retireAgent('ada@a.test', at(6));
|
|
165
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'k3', added: at(7), revoked: null }), 'retired');
|
|
166
|
+
assert.ok((await store.agentByAddress('ada@a.test')).keys.every(k => k.revoked !== null));
|
|
167
|
+
});
|
|
168
|
+
it('keeps only a key the agent holds, revoking every other, and nothing for a key it does not hold', async () => {
|
|
169
|
+
await store.createAgent(agent('ada@a.test', 'k0', 'k1', 'k2'));
|
|
170
|
+
await store.createAgent(agent('bob@a.test', 'kb'));
|
|
171
|
+
assert.equal(await store.keepOnlyKey('ada@a.test', 'kb', at(5)), 'not_held');
|
|
172
|
+
assert.equal(await store.keepOnlyKey('ada@a.test', 'k9', at(5)), 'not_held');
|
|
173
|
+
assert.equal(await store.keepOnlyKey('ada@a.test', 'k1', at(5)), 'kept');
|
|
174
|
+
const keys = (await store.agentByAddress('ada@a.test')).keys;
|
|
175
|
+
assert.deepEqual(keys.map(k => [k.key, k.revoked]), [
|
|
176
|
+
['k0', at(5)],
|
|
177
|
+
['k1', null],
|
|
178
|
+
['k2', at(5)],
|
|
179
|
+
]);
|
|
180
|
+
assert.equal(await store.keepOnlyKey('ada@a.test', 'k0', at(6)), 'not_held', 'a revoked key is not held');
|
|
181
|
+
await store.retireAgent('bob@a.test', at(7));
|
|
182
|
+
assert.equal(await store.keepOnlyKey('bob@a.test', 'kb', at(8)), 'not_held', 'nor a retired agent');
|
|
183
|
+
});
|
|
184
|
+
it('separates current recovery authority from future historical revocations', async () => {
|
|
185
|
+
for (const mode of ['principal', 'mailbox', 'keep']) {
|
|
186
|
+
const address = `${mode}@a.test`, a = agent(address, `${mode}-old`, `${mode}-keep`);
|
|
187
|
+
a.keys[0].revoked = at(100 * HOUR);
|
|
188
|
+
await store.createAgent(a);
|
|
189
|
+
if (mode === 'keep')
|
|
190
|
+
assert.equal(await store.keepOnlyKey(address, `${mode}-keep`, at(1)), 'kept');
|
|
191
|
+
else if (mode === 'principal')
|
|
192
|
+
assert.equal(await store.replaceKeys(address, { key: `${mode}-new`, added: at(1), revoked: null }), 'replaced');
|
|
193
|
+
else {
|
|
194
|
+
const pending = { key: `${mode}-new`, hash: 'h', completesAt: at(1) };
|
|
195
|
+
await store.beginRecovery(address, pending, at(2), at());
|
|
196
|
+
assert.equal(await store.completeRecovery(address, { key: pending.key, added: at(1), revoked: null }, pending), 'replaced');
|
|
197
|
+
}
|
|
198
|
+
const current = (await store.agentByAddress(address));
|
|
199
|
+
assert.equal(current.keys[0].revoked, at(100 * HOUR), 'historical revocation does not move');
|
|
200
|
+
assert.deepEqual(current.authorizedKeys, [`${mode}-${mode === 'keep' ? 'keep' : 'new'}`]);
|
|
201
|
+
assert.equal(await store.revokeKey(address, current.authorizedKeys[0], at(1), a => a.authorizedKeys?.includes(`${mode}-old`) ?? true), 'not_found', 'old signer cannot use preserved history as current authority');
|
|
202
|
+
await store.addKey(address, { key: `${mode}-added`, added: at(2), revoked: null }, 32);
|
|
203
|
+
assert.ok((await store.agentByAddress(address)).authorizedKeys.includes(`${mode}-added`));
|
|
204
|
+
await store.retireAgent(address, at(3));
|
|
205
|
+
assert.deepEqual((await store.agentByAddress(address)).authorizedKeys, []);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
it('returns copies', async () => {
|
|
209
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
210
|
+
const a = (await store.agentByAddress('ada@a.test'));
|
|
211
|
+
a.keys.push({ key: 'x', added: at(), revoked: null });
|
|
212
|
+
assert.equal((await store.agentByAddress('ada@a.test')).keys.length, 1);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('messages', () => {
|
|
216
|
+
it('stores a message once, and the first record wins', async () => {
|
|
217
|
+
const m = message('ada@a.test', ['bob@b.test']);
|
|
218
|
+
assert.deepEqual(await store.putMessage(m), m);
|
|
219
|
+
assert.deepEqual(await store.putMessage({ ...m, carrier: 'https://other.test' }), m);
|
|
220
|
+
assert.deepEqual(await store.message(m.id), m);
|
|
221
|
+
assert.equal(await store.message('f'.repeat(64)), null);
|
|
222
|
+
});
|
|
223
|
+
it('stores a message with its rows, and a repeat adds only the rows that are missing', async () => {
|
|
224
|
+
const m = message('ada@a.test', ['bob@b.test', 'cat@c.test']);
|
|
225
|
+
await store.putMessage(m, { deliveries: [delivery(m.id, 'b.test')] });
|
|
226
|
+
await store.putMessage(m, {
|
|
227
|
+
deliveries: [delivery(m.id, 'b.test', { recipients: ['changed@b.test'] }), delivery(m.id, 'c.test')],
|
|
228
|
+
});
|
|
229
|
+
assert.deepEqual((await store.deliveries(m.id)).map(d => [d.domain, d.recipients]), [
|
|
230
|
+
['b.test', ['bob@b.test']],
|
|
231
|
+
['c.test', ['bob@c.test']],
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
it('deletes a message with everything tied to it', async () => {
|
|
235
|
+
const m = message('ada@a.test', ['bob@b.test'], { parts: [{ type: 'a/b', sha256: sha('a'), size: 1 }] });
|
|
236
|
+
await store.putMessage(m, { arrivals: [arrival(m.id, 'bob@b.test')], deliveries: [delivery(m.id, 'b.test')] });
|
|
237
|
+
await store.addToInbox('bob@b.test', m.id, at());
|
|
238
|
+
await store.deleteMessage(m.id);
|
|
239
|
+
assert.equal(await store.message(m.id), null);
|
|
240
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 10, at())).messages, []);
|
|
241
|
+
assert.deepEqual((await store.sync('ada@a.test', 0, 10, at())).messages, []);
|
|
242
|
+
assert.deepEqual(await store.inboxEvents(10), []);
|
|
243
|
+
assert.deepEqual(await store.deliveries(m.id), []);
|
|
244
|
+
assert.deepEqual(await store.arrivals(at(), 10), []);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe('arrivals', () => {
|
|
248
|
+
it('queues arrivals per recipient, due in order, each retried on its own', async () => {
|
|
249
|
+
const a = message('ada@a.test', ['bob@b.test', 'cat@b.test']);
|
|
250
|
+
const b = message('ada@a.test', ['bob@b.test']);
|
|
251
|
+
await store.putMessage(a, {
|
|
252
|
+
arrivals: [arrival(a.id, 'bob@b.test', { hints: { invite: 'x' } }), arrival(a.id, 'cat@b.test')],
|
|
253
|
+
});
|
|
254
|
+
await store.putMessage(b, { arrivals: [arrival(b.id, 'bob@b.test', { dueAt: at(1) })] });
|
|
255
|
+
await store.putMessage(a, { arrivals: [arrival(a.id, 'bob@b.test', { hints: { changed: 'y' } })] });
|
|
256
|
+
const due = await store.arrivals(at(1), 10);
|
|
257
|
+
assert.deepEqual(due.map(d => [d.message.id, d.arrival.recipient]), [
|
|
258
|
+
[a.id, 'bob@b.test'],
|
|
259
|
+
[a.id, 'cat@b.test'],
|
|
260
|
+
[b.id, 'bob@b.test'],
|
|
261
|
+
]);
|
|
262
|
+
assert.deepEqual(due[0].arrival.hints, { invite: 'x' });
|
|
263
|
+
assert.deepEqual(due[0].message, a);
|
|
264
|
+
assert.equal((await store.arrivals(at(0), 10)).length, 2, 'b is not due yet');
|
|
265
|
+
await store.arrivalRetry(a.id, 'bob@b.test', at(HOUR));
|
|
266
|
+
const later = await store.arrivals(at(HOUR), 10);
|
|
267
|
+
assert.deepEqual(later.map(d => [d.arrival.recipient, d.arrival.attempts]), [
|
|
268
|
+
['cat@b.test', 0],
|
|
269
|
+
['bob@b.test', 0],
|
|
270
|
+
['bob@b.test', 1],
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
it('keeps a received message while an arrival waits or once one kept it, and drops it otherwise', async () => {
|
|
274
|
+
const kept = message('eve@e.test', ['bob@b.test', 'cat@b.test']);
|
|
275
|
+
await store.putMessage(kept, { arrivals: [arrival(kept.id, 'bob@b.test'), arrival(kept.id, 'cat@b.test')] });
|
|
276
|
+
assert.equal(await store.arrivalDone(kept.id, 'bob@b.test', false), null, 'cat is still waiting');
|
|
277
|
+
assert.ok(await store.message(kept.id));
|
|
278
|
+
assert.equal(await store.arrivalDone(kept.id, 'cat@b.test', true), 'kept');
|
|
279
|
+
assert.ok(await store.message(kept.id));
|
|
280
|
+
const dropped = message('eve@e.test', ['bob@b.test']);
|
|
281
|
+
await store.putMessage(dropped, { arrivals: [arrival(dropped.id, 'bob@b.test')] });
|
|
282
|
+
assert.equal(await store.arrivalDone(dropped.id, 'bob@b.test', false), 'dropped');
|
|
283
|
+
assert.equal(await store.message(dropped.id), null);
|
|
284
|
+
const offered = message('eve@e.test', ['bob@b.test']);
|
|
285
|
+
await store.putMessage(offered, { arrivals: [arrival(offered.id, 'bob@b.test')] });
|
|
286
|
+
await store.addToInbox('dan@b.test', offered.id, at());
|
|
287
|
+
assert.equal(await store.arrivalDone(offered.id, 'bob@b.test', false), null, 'an inbox holds it');
|
|
288
|
+
assert.ok(await store.message(offered.id));
|
|
289
|
+
const sent = message('ada@a.test', ['bob@b.test']);
|
|
290
|
+
await store.putMessage(sent);
|
|
291
|
+
await store.putMessage(sent, { arrivals: [arrival(sent.id, 'bob@b.test')] });
|
|
292
|
+
assert.equal(await store.arrivalDone(sent.id, 'bob@b.test', false), null, 'a message written here stays');
|
|
293
|
+
assert.ok(await store.message(sent.id));
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe('atomic recovery authority', () => {
|
|
297
|
+
const proof = (key) => ({ key, hash: 'proof-hash', completesAt: at(1) });
|
|
298
|
+
const epoch = async (address) => (await store.agentByAddress(address)).recoveryEpoch ?? 0;
|
|
299
|
+
it('advances the recovery epoch when a recovery ends or authority resets, never for a key added or revoked', async () => {
|
|
300
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
301
|
+
assert.equal(await epoch('ada@a.test'), 0);
|
|
302
|
+
assert.equal(await store.addKey('ada@a.test', { key: 'k1', added: at(1), revoked: null }, 8), 'added');
|
|
303
|
+
assert.equal(await store.revokeKey('ada@a.test', 'k1', at(2)), 'revoked');
|
|
304
|
+
assert.equal(await epoch('ada@a.test'), 0, 'key churn leaves mailbox codes good');
|
|
305
|
+
const pending = proof('k2');
|
|
306
|
+
await store.beginRecovery('ada@a.test', pending, null, at(3));
|
|
307
|
+
assert.equal(await epoch('ada@a.test'), 0, 'beginning one does not');
|
|
308
|
+
assert.equal(await store.cancelRecovery('ada@a.test', pending, at(4)), true);
|
|
309
|
+
assert.equal(await epoch('ada@a.test'), 1, 'cancelled');
|
|
310
|
+
await store.beginRecovery('ada@a.test', pending, null, at(5));
|
|
311
|
+
assert.equal(await store.completeRecovery('ada@a.test', { key: 'k2', added: at(6), revoked: null }, pending), 'replaced');
|
|
312
|
+
assert.equal(await epoch('ada@a.test'), 2, 'completed');
|
|
313
|
+
await store.beginRecovery('ada@a.test', proof('k3'), at(HOUR), at(7));
|
|
314
|
+
await store.sweep(at(2 * HOUR), at(2 * HOUR));
|
|
315
|
+
assert.equal(await store.value('recovery', 'ada@a.test', at(2 * HOUR)), null);
|
|
316
|
+
assert.equal(await epoch('ada@a.test'), 3, 'lapsed');
|
|
317
|
+
assert.equal(await store.keepOnlyKey('ada@a.test', 'k2', at(2 * HOUR)), 'kept');
|
|
318
|
+
assert.equal(await epoch('ada@a.test'), 4, 'principal recovery keeping a key');
|
|
319
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'k4', added: at(2 * HOUR), revoked: null }), 'replaced');
|
|
320
|
+
assert.equal(await epoch('ada@a.test'), 5, 'principal recovery replacing keys');
|
|
321
|
+
await store.retireAgent('ada@a.test', at(3 * HOUR));
|
|
322
|
+
assert.equal(await epoch('ada@a.test'), 6, 'retired');
|
|
323
|
+
});
|
|
324
|
+
it('bounds stored key history for additions, never for recovery', async () => {
|
|
325
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
326
|
+
for (let i = 1; i < MAX_STORED_KEYS; i++) {
|
|
327
|
+
assert.equal(await store.addKey('ada@a.test', { key: `k${i}`, added: at(i), revoked: null }, 8), 'added');
|
|
328
|
+
assert.equal(await store.revokeKey('ada@a.test', `k${i}`, at(i)), 'revoked');
|
|
329
|
+
}
|
|
330
|
+
assert.equal((await store.agentByAddress('ada@a.test')).keys.length, MAX_STORED_KEYS);
|
|
331
|
+
assert.equal(await store.addKey('ada@a.test', { key: 'over', added: at(HOUR), revoked: null }, 8), 'too_many_keys');
|
|
332
|
+
assert.equal(await store.agentByKey('over'), null);
|
|
333
|
+
const pending = proof('mailbox');
|
|
334
|
+
await store.beginRecovery('ada@a.test', pending, null, at(HOUR));
|
|
335
|
+
assert.equal(await store.completeRecovery('ada@a.test', { key: 'mailbox', added: at(HOUR), revoked: null }, pending), 'replaced');
|
|
336
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'principal', added: at(HOUR), revoked: null }), 'replaced');
|
|
337
|
+
assert.equal((await store.agentByAddress('ada@a.test')).keys.length, MAX_STORED_KEYS + 2);
|
|
338
|
+
});
|
|
339
|
+
it('refuses replacing keys of a retired agent as retired, even with a key in use', async () => {
|
|
340
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
341
|
+
await store.createAgent(agent('bob@a.test', 'b0'));
|
|
342
|
+
await store.retireAgent('ada@a.test', at(1));
|
|
343
|
+
assert.equal(await store.replaceKeys('ada@a.test', { key: 'b0', added: at(2), revoked: null }), 'retired');
|
|
344
|
+
assert.equal(await store.replaceKeys('bob@a.test', { key: 'k0', added: at(2), revoked: null }), 'key_in_use');
|
|
345
|
+
});
|
|
346
|
+
it('rejects unsafe generic writes to the reserved recovery namespace', async () => {
|
|
347
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
348
|
+
const pending = proof('k1');
|
|
349
|
+
await store.beginRecovery('ada@a.test', pending, null, at());
|
|
350
|
+
await assert.rejects(store.setValue('recovery', 'ada@a.test', proof('k2'), null), /reserved/);
|
|
351
|
+
await assert.rejects(store.valueOnce('recovery', 'ada@a.test', proof('k2'), null, at()), /reserved/);
|
|
352
|
+
await assert.rejects(store.releaseValue('recovery', 'ada@a.test', pending), /reserved/);
|
|
353
|
+
await assert.rejects(store.renewValue('recovery', 'ada@a.test', pending, at(HOUR)), /reserved/);
|
|
354
|
+
assert.deepEqual(await store.value('recovery', 'ada@a.test', at()), pending);
|
|
355
|
+
});
|
|
356
|
+
it('preserves pending proof on capacity failure and completes keys plus proof in one change', async () => {
|
|
357
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
358
|
+
const pending = proof('k1');
|
|
359
|
+
await store.beginRecovery('ada@a.test', pending, null, at());
|
|
360
|
+
const before = await store.agentByAddress('ada@a.test');
|
|
361
|
+
assert.equal(await store.completeRecovery('ada@a.test', { key: 'k1', added: at(2), revoked: null }, pending, () => false), 'too_many_keys');
|
|
362
|
+
assert.deepEqual(await store.value('recovery', 'ada@a.test', at(2)), pending);
|
|
363
|
+
assert.deepEqual(await store.agentByAddress('ada@a.test'), before);
|
|
364
|
+
assert.equal(await store.completeRecovery('ada@a.test', { key: 'k1', added: at(2), revoked: null }, pending), 'replaced');
|
|
365
|
+
assert.equal(await store.value('recovery', 'ada@a.test', at(2)), null);
|
|
366
|
+
assert.equal((await store.agentByAddress('ada@a.test')).keys.find(k => k.key === 'k1').revoked, null);
|
|
367
|
+
assert.equal(await store.completeRecovery('ada@a.test', { key: 'k1', added: at(3), revoked: null }, pending), 'not_pending');
|
|
368
|
+
});
|
|
369
|
+
it('cannot complete explicitly undefended recovery, even after its waiting time, until exact activation', async () => {
|
|
370
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
371
|
+
const pending = { ...proof('k1'), defended: false, nonce: 'unchanged', cancel: { binding: 'same' } };
|
|
372
|
+
await store.beginRecovery('ada@a.test', pending, null, at());
|
|
373
|
+
const entry = { key: 'k1', added: at(HOUR), revoked: null };
|
|
374
|
+
assert.equal(await store.completeRecovery('ada@a.test', entry, pending), 'not_pending');
|
|
375
|
+
assert.deepEqual(await store.value('recovery', 'ada@a.test', at(HOUR)), pending);
|
|
376
|
+
assert.equal(await store.agentByKey('k1'), null);
|
|
377
|
+
assert.equal(await store.activateRecovery('ada@a.test', { ...pending, hash: 'wrong' }, at(HOUR)), false);
|
|
378
|
+
const activated = { ...pending, defended: true };
|
|
379
|
+
const results = await Promise.all([
|
|
380
|
+
store.activateRecovery('ada@a.test', pending, at(HOUR)),
|
|
381
|
+
store.activateRecovery('ada@a.test', pending, at(HOUR)),
|
|
382
|
+
]);
|
|
383
|
+
assert.deepEqual(results.sort(), [false, true]);
|
|
384
|
+
assert.deepEqual(await store.value('recovery', 'ada@a.test', at(HOUR)), activated);
|
|
385
|
+
assert.equal(await store.completeRecovery('ada@a.test', entry, pending), 'not_pending');
|
|
386
|
+
await assert.rejects(store.completeRecovery('ada@a.test', entry, activated, () => {
|
|
387
|
+
throw new Error('admission unavailable');
|
|
388
|
+
}), /admission unavailable/);
|
|
389
|
+
assert.deepEqual(await store.value('recovery', 'ada@a.test', at(HOUR)), activated);
|
|
390
|
+
assert.equal(await store.agentByKey('k1'), null);
|
|
391
|
+
assert.equal(await store.completeRecovery('ada@a.test', entry, activated), 'replaced');
|
|
392
|
+
});
|
|
393
|
+
it('cannot activate expired, cancelled or principal-cleared recovery and preserves legacy completion', async () => {
|
|
394
|
+
for (const mode of ['expired', 'cancelled', 'principal', 'retired']) {
|
|
395
|
+
const address = `${mode}@a.test`;
|
|
396
|
+
await store.createAgent(agent(address, `${mode}-old`));
|
|
397
|
+
const pending = { ...proof(`${mode}-new`), defended: false };
|
|
398
|
+
await store.beginRecovery(address, pending, mode === 'expired' ? at(2) : null, at());
|
|
399
|
+
if (mode === 'cancelled')
|
|
400
|
+
assert.equal(await store.cancelRecovery(address, pending, at(2)), true);
|
|
401
|
+
if (mode === 'principal')
|
|
402
|
+
assert.equal(await store.keepOnlyKey(address, `${mode}-old`, at(2)), 'kept');
|
|
403
|
+
if (mode === 'retired')
|
|
404
|
+
await store.retireAgent(address, at(2));
|
|
405
|
+
assert.equal(await store.activateRecovery(address, pending, at(2)), false);
|
|
406
|
+
}
|
|
407
|
+
await store.createAgent(agent('legacy@a.test', 'legacy-old'));
|
|
408
|
+
const pending = proof('legacy-new');
|
|
409
|
+
await store.beginRecovery('legacy@a.test', pending, null, at());
|
|
410
|
+
assert.equal(await store.completeRecovery('legacy@a.test', { key: 'legacy-new', added: at(2), revoked: null }, pending), 'replaced');
|
|
411
|
+
});
|
|
412
|
+
it('fences a delayed begin even when principal recovery keeps the already-only key', async () => {
|
|
413
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
414
|
+
const old = await store.agentByAddress('ada@a.test');
|
|
415
|
+
const pending = proof('k1');
|
|
416
|
+
await store.beginRecovery('ada@a.test', pending, null, at());
|
|
417
|
+
assert.equal(await store.keepOnlyKey('ada@a.test', 'k0', at(2)), 'kept');
|
|
418
|
+
assert.equal(await store.value('recovery', 'ada@a.test', at(2)), null);
|
|
419
|
+
assert.equal(await store.beginRecovery('ada@a.test', proof('k2'), null, at(2), a => (a.authorityVersion ?? 0) === (old.authorityVersion ?? 0)), null);
|
|
420
|
+
assert.equal(await store.completeRecovery('ada@a.test', { key: 'k1', added: at(3), revoked: null }, pending), 'not_pending');
|
|
421
|
+
assert.deepEqual((await store.agentByAddress('ada@a.test')).keys, old.keys);
|
|
422
|
+
});
|
|
423
|
+
it('atomically persists key-cancellation history and invalidates stale begin authority', async () => {
|
|
424
|
+
await store.createAgent(agent('ada@a.test', 'k0'));
|
|
425
|
+
const captured = await store.agentByAddress('ada@a.test');
|
|
426
|
+
const pending = proof('k1');
|
|
427
|
+
await store.beginRecovery('ada@a.test', pending, null, at());
|
|
428
|
+
assert.equal(await store.cancelRecovery('ada@a.test', pending, at(2), () => false, at(2)), false);
|
|
429
|
+
assert.equal(await store.cancelRecovery('ada@a.test', pending, at(2), () => true, at(2)), true);
|
|
430
|
+
const current = (await store.agentByAddress('ada@a.test'));
|
|
431
|
+
assert.equal(current.recoveryKeyCancelledAt, at(2));
|
|
432
|
+
assert.ok((current.authorityVersion ?? 0) > (captured.authorityVersion ?? 0));
|
|
433
|
+
assert.equal(await store.beginRecovery('ada@a.test', proof('k2'), null, at(3), a => (a.authorityVersion ?? 0) === (captured.authorityVersion ?? 0)), null);
|
|
434
|
+
const next = proof('k2');
|
|
435
|
+
assert.deepEqual(await store.beginRecovery('ada@a.test', next, null, at(3)), next);
|
|
436
|
+
assert.equal(await store.cancelRecovery('ada@a.test', pending, at(4), () => true, at(4)), false);
|
|
437
|
+
assert.deepEqual(await store.value('recovery', 'ada@a.test', at(4)), next);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
describe('sync streams', () => {
|
|
441
|
+
it('never applies a late arrival decision to a recreated message incarnation', async () => {
|
|
442
|
+
const old = { ...message('eve@e.test', ['ada@a.test']), carrier: 'https://e.test' };
|
|
443
|
+
await store.putMessage(old, { arrivals: [arrival(old.id, 'ada@a.test')] });
|
|
444
|
+
const [snapshot] = await store.arrivals(at(), 1);
|
|
445
|
+
await store.deleteMessage(old.id);
|
|
446
|
+
await store.putMessage({ ...old, carrier: 'https://new.test' }, { arrivals: [arrival(old.id, 'ada@a.test')] });
|
|
447
|
+
const [fresh] = await store.arrivals(at(), 1);
|
|
448
|
+
assert.notEqual(fresh.generation, snapshot.generation);
|
|
449
|
+
await store.arrivalRetry(old.id, 'ada@a.test', at(HOUR), snapshot.generation);
|
|
450
|
+
assert.equal(await store.arrivalDone(old.id, 'ada@a.test', true, snapshot.generation), null);
|
|
451
|
+
assert.equal(await store.addToInbox('ada@a.test', old.id, at(), snapshot.generation), false);
|
|
452
|
+
const [stillWaiting] = await store.arrivals(at(), 1);
|
|
453
|
+
assert.equal(stillWaiting.arrival.attempts, 0);
|
|
454
|
+
assert.equal(stillWaiting.generation, fresh.generation);
|
|
455
|
+
assert.equal(await store.addToInbox('ada@a.test', old.id, at(), fresh.generation), true);
|
|
456
|
+
});
|
|
457
|
+
it('keeps one inbox entry per address and message, paged by position and limit', async () => {
|
|
458
|
+
const ms = [1, 2, 3].map(() => message('ada@a.test', ['bob@b.test']));
|
|
459
|
+
for (const m of ms)
|
|
460
|
+
await store.putMessage(m);
|
|
461
|
+
for (const m of ms)
|
|
462
|
+
assert.equal(await store.addToInbox('bob@b.test', m.id, at(1)), true);
|
|
463
|
+
assert.equal(await store.addToInbox('bob@b.test', ms[0].id, at(2)), false);
|
|
464
|
+
const all = await store.sync('bob@b.test', 0, 10, at());
|
|
465
|
+
assert.deepEqual(ids(all), ms.map(m => m.id));
|
|
466
|
+
assert.equal(all.more, false);
|
|
467
|
+
const [first, second, third] = all.messages;
|
|
468
|
+
assert.ok(first.cursor < second.cursor && second.cursor < third.cursor);
|
|
469
|
+
assert.deepEqual([first.address, first.receivedAt, first.message], ['bob@b.test', at(1), ms[0].message]);
|
|
470
|
+
const page = await store.sync('bob@b.test', first.cursor, 1, at());
|
|
471
|
+
assert.deepEqual([ids(page), page.cursor, page.more], [[ms[1].id], second.cursor, true]);
|
|
472
|
+
const rest = await store.sync('bob@b.test', page.cursor, 5, at());
|
|
473
|
+
assert.deepEqual([ids(rest), rest.more], [[ms[2].id], false]);
|
|
474
|
+
assert.deepEqual(ids(await store.sync('eve@b.test', 0, 10, at())), []);
|
|
475
|
+
});
|
|
476
|
+
it('holds what an agent wrote here beside what it received, in the order the store took them', async () => {
|
|
477
|
+
const wrote = message('ada@a.test', ['bob@b.test']);
|
|
478
|
+
const received = { ...message('bob@b.test', ['ada@a.test']), carrier: 'https://b.test' };
|
|
479
|
+
const published = message('ada@a.test', []);
|
|
480
|
+
await store.putMessage(wrote);
|
|
481
|
+
await store.putMessage(received, { arrivals: [arrival(received.id, 'ada@a.test')] });
|
|
482
|
+
assert.deepEqual(ids(await store.sync('ada@a.test', 0, 10, at())), [wrote.id], 'not yet taken');
|
|
483
|
+
await store.addToInbox('ada@a.test', received.id, at(5));
|
|
484
|
+
await store.putMessage(published);
|
|
485
|
+
await store.putMessage(wrote, { deliveries: [delivery(wrote.id, 'b.test')] });
|
|
486
|
+
const page = await store.sync('ada@a.test', 0, 10, at());
|
|
487
|
+
assert.deepEqual(ids(page), [wrote.id, received.id, published.id], 'a repeat takes no new place');
|
|
488
|
+
assert.deepEqual(page.messages.map(e => [e.address, e.receivedAt]), [
|
|
489
|
+
['ada@a.test', wrote.receivedAt],
|
|
490
|
+
['ada@a.test', at(5)],
|
|
491
|
+
['ada@a.test', published.receivedAt],
|
|
492
|
+
]);
|
|
493
|
+
assert.deepEqual(ids(await store.sync('bob@b.test', 0, 10, at())), [], 'the author of a received message');
|
|
494
|
+
});
|
|
495
|
+
it('starts from the messages that entered a stream at or after a time', async () => {
|
|
496
|
+
const [old, recent] = [1, 2].map(() => message('ada@a.test', ['bob@b.test']));
|
|
497
|
+
for (const m of [old, recent])
|
|
498
|
+
await store.putMessage(m);
|
|
499
|
+
await store.addToInbox('bob@b.test', old.id, at(1));
|
|
500
|
+
await store.addToInbox('bob@b.test', recent.id, at(HOUR));
|
|
501
|
+
assert.deepEqual(ids(await store.sync('bob@b.test', 0, 10, at(), at(HOUR))), [recent.id]);
|
|
502
|
+
assert.deepEqual(ids(await store.sync('bob@b.test', 0, 10, at(), at(HOUR + 1))), []);
|
|
503
|
+
});
|
|
504
|
+
it("covers every position taken so far, other agents' too, once nothing more is there", async () => {
|
|
505
|
+
const m = message('ada@a.test', ['bob@b.test']);
|
|
506
|
+
await store.putMessage(m);
|
|
507
|
+
await store.addToInbox('bob@b.test', m.id, at());
|
|
508
|
+
const bob = await store.sync('bob@b.test', 0, 10, at());
|
|
509
|
+
const cat = await store.sync('cat@b.test', 0, 10, at());
|
|
510
|
+
assert.equal(cat.cursor, bob.cursor);
|
|
511
|
+
assert.equal(cat.cursor, bob.messages[0].cursor);
|
|
512
|
+
const later = await store.sync('cat@b.test', cat.cursor, 10, at());
|
|
513
|
+
assert.deepEqual([later.messages, later.cursor, later.more], [[], cat.cursor, false]);
|
|
514
|
+
assert.ok((await store.sync('cat@b.test', cat.cursor + 5, 10, at())).cursor < cat.cursor + 5);
|
|
515
|
+
});
|
|
516
|
+
it('retains a late-admitted message for the complete promised sync window', async () => {
|
|
517
|
+
const day = 24 * HOUR;
|
|
518
|
+
const m = { ...message('eve@e.test', ['ada@a.test']), expiresAt: at(30 * day) };
|
|
519
|
+
await store.putMessage(m, { arrivals: [arrival(m.id, 'ada@a.test')] });
|
|
520
|
+
const [arrivalSnapshot] = await store.arrivals(at(), 1);
|
|
521
|
+
assert.equal(await store.addToInbox('ada@a.test', m.id, at(29 * day), arrivalSnapshot.generation, at(36 * day)), true);
|
|
522
|
+
await store.sweep(at(30 * day), at());
|
|
523
|
+
assert.deepEqual(ids(await store.sync('ada@a.test', 0, 10, at(35 * day))), [m.id]);
|
|
524
|
+
await store.sweep(at(36 * day), at());
|
|
525
|
+
assert.equal(await store.message(m.id), null);
|
|
526
|
+
});
|
|
527
|
+
it('shows a held signal in its place until it expires, and forgets it', async () => {
|
|
528
|
+
const [m1, m2] = [1, 2].map(() => message('ada@a.test', ['bob@b.test']));
|
|
529
|
+
for (const m of [m1, m2])
|
|
530
|
+
await store.putMessage(m);
|
|
531
|
+
const signal = { type: 'read', from: 'ada@a.test', about: m1.id, at: at() };
|
|
532
|
+
await store.addToInbox('bob@b.test', m1.id, at());
|
|
533
|
+
await store.holdSignal('bob@b.test', signal, at(300_000), at());
|
|
534
|
+
await store.holdSignal('cat@b.test', { ...signal, type: 'typing' }, at(300_000), at());
|
|
535
|
+
await store.addToInbox('bob@b.test', m2.id, at());
|
|
536
|
+
const all = await store.sync('bob@b.test', 0, 10, at(1));
|
|
537
|
+
assert.deepEqual(all.signals.map(({ cursor: _, ...held }) => held), [signal]);
|
|
538
|
+
const [e1, e2] = all.messages;
|
|
539
|
+
assert.ok(e1.cursor < all.signals[0].cursor && all.signals[0].cursor < e2.cursor);
|
|
540
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 1, at(1))).signals, [], 'not on a page that ends before it');
|
|
541
|
+
assert.equal((await store.sync('bob@b.test', e1.cursor, 1, at(1))).signals.length, 1);
|
|
542
|
+
assert.deepEqual((await store.sync('bob@b.test', all.signals[0].cursor, 10, at(1))).signals, []);
|
|
543
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 10, at(300_000))).signals, [], 'expired');
|
|
544
|
+
await store.holdSignal('bob@b.test', { ...signal, type: 'typing' }, at(600_000), at(300_000));
|
|
545
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 10, at(299_999))).signals.map(s => s.type), ['typing'], 'the expired ones are forgotten');
|
|
546
|
+
assert.equal((await store.sweep(at(600_000), at())).signals, 1);
|
|
547
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 10, at())).signals, []);
|
|
548
|
+
});
|
|
549
|
+
it('queues one inbox event with each new entry, until it is told', async () => {
|
|
550
|
+
const [m1, m2] = [1, 2].map(() => message('ada@a.test', ['bob@b.test', 'cat@b.test']));
|
|
551
|
+
for (const m of [m1, m2])
|
|
552
|
+
await store.putMessage(m);
|
|
553
|
+
await store.addToInbox('bob@b.test', m1.id, at());
|
|
554
|
+
await store.addToInbox('cat@b.test', m1.id, at());
|
|
555
|
+
await store.addToInbox('bob@b.test', m1.id, at());
|
|
556
|
+
await store.addToInbox('bob@b.test', m2.id, at());
|
|
557
|
+
const events = await store.inboxEvents(10);
|
|
558
|
+
assert.deepEqual(events.map(e => [e.address, e.id]), [
|
|
559
|
+
['bob@b.test', m1.id],
|
|
560
|
+
['cat@b.test', m1.id],
|
|
561
|
+
['bob@b.test', m2.id],
|
|
562
|
+
]);
|
|
563
|
+
assert.deepEqual(events[0].message, m1.message);
|
|
564
|
+
await store.inboxEventDone(events[1].cursor);
|
|
565
|
+
assert.deepEqual((await store.inboxEvents(1)).map(e => e.cursor), [events[0].cursor]);
|
|
566
|
+
assert.equal((await store.inboxEvents(10)).length, 2);
|
|
567
|
+
});
|
|
568
|
+
it('never lets a reader following the cursor skip an entry that concurrent writers add', async () => {
|
|
569
|
+
const ms = Array.from({ length: 120 }, () => message('ada@a.test', ['bob@b.test']));
|
|
570
|
+
for (const m of ms)
|
|
571
|
+
await store.putMessage(m);
|
|
572
|
+
const seen = new Set();
|
|
573
|
+
let writing = true;
|
|
574
|
+
const reader = (async () => {
|
|
575
|
+
for (let cursor = 0;;) {
|
|
576
|
+
const last = !writing;
|
|
577
|
+
const page = await store.sync('bob@b.test', cursor, 7, at());
|
|
578
|
+
for (const e of page.messages)
|
|
579
|
+
seen.add(e.id);
|
|
580
|
+
cursor = page.cursor;
|
|
581
|
+
if (last && !page.more)
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
await Promise.all(ms.map((m, i) => i % 3
|
|
586
|
+
? store.addToInbox('bob@b.test', m.id, at())
|
|
587
|
+
: store.holdSignal('bob@b.test', { type: 'typing', from: 'ada@a.test', about: m.id, at: at() }, at(1), at())));
|
|
588
|
+
writing = false;
|
|
589
|
+
await reader;
|
|
590
|
+
assert.equal(seen.size, ms.filter((_, i) => i % 3).length);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
describe('webhooks', () => {
|
|
594
|
+
const hook = (id, address = 'ada@a.test', extra = {}) => ({
|
|
595
|
+
id,
|
|
596
|
+
address,
|
|
597
|
+
url: `https://hooks.test/${id}`,
|
|
598
|
+
codeHash: sha('c'),
|
|
599
|
+
verified: false,
|
|
600
|
+
createdAt: at(),
|
|
601
|
+
failingSince: null,
|
|
602
|
+
...extra,
|
|
603
|
+
});
|
|
604
|
+
const claimed = async (now, quiet = now + 1000) => (await store.claimWebhooks(at(now), 10, at(quiet), lease(`w${now}`, at(now + 30_000)))).map(w => w.id).sort();
|
|
605
|
+
it('keeps at most so many per agent, even when adds race', async () => {
|
|
606
|
+
const added = await Promise.all(['w1', 'w2', 'w3', 'w4'].map(id => store.addWebhook(hook(id), 2)));
|
|
607
|
+
assert.equal(added.filter(Boolean).length, 2);
|
|
608
|
+
assert.equal(await store.addWebhook(hook('w5', 'bob@a.test'), 2), true);
|
|
609
|
+
assert.deepEqual(await store.webhook('w5'), hook('w5', 'bob@a.test'));
|
|
610
|
+
assert.equal(await store.webhook('nope'), null);
|
|
611
|
+
const [kept] = ['w1', 'w2', 'w3', 'w4'].filter((_, i) => added[i]);
|
|
612
|
+
await store.deleteWebhook(kept);
|
|
613
|
+
assert.equal(await store.webhook(kept), null);
|
|
614
|
+
assert.equal(await store.addWebhook(hook('w6'), 2), true, 'a deleted one frees its place');
|
|
615
|
+
});
|
|
616
|
+
it('wakes only verified webhooks, each claimed once and then quiet for a while', async () => {
|
|
617
|
+
await store.addWebhook(hook('w1'), 5);
|
|
618
|
+
await store.addWebhook(hook('w2'), 5);
|
|
619
|
+
await store.addWebhook(hook('w3', 'bob@a.test'), 5);
|
|
620
|
+
await store.wakeWebhooks('ada@a.test', at());
|
|
621
|
+
assert.deepEqual(await claimed(0), [], 'none verified');
|
|
622
|
+
for (const id of ['w1', 'w3'])
|
|
623
|
+
await store.verifyWebhook(id);
|
|
624
|
+
assert.equal((await store.webhook('w1'))?.verified, true);
|
|
625
|
+
await store.wakeWebhooks('ada@a.test', at(10));
|
|
626
|
+
assert.deepEqual(await claimed(9), [], 'not due yet');
|
|
627
|
+
const racing = await Promise.all([1, 2, 3].map(i => store.claimWebhooks(at(10), 10, at(1010), lease(`race${i}`, at(30_010)))));
|
|
628
|
+
assert.deepEqual(racing.flat().map(w => w.id), ['w1']);
|
|
629
|
+
await store.wakeWebhooks('ada@a.test', at(20));
|
|
630
|
+
await store.wakeWebhooks('ada@a.test', at(30));
|
|
631
|
+
assert.deepEqual(await claimed(1009), [], 'quiet until 1010');
|
|
632
|
+
const won = racing.findIndex(a => a.length > 0) + 1;
|
|
633
|
+
assert.equal(await store.settleWebhook('w1', null, null, `race${won}`), true);
|
|
634
|
+
assert.deepEqual(await claimed(1010, 2010), ['w1'], 'one post for both wakes');
|
|
635
|
+
await store.settleWebhook('w1', null, null, 'w1010');
|
|
636
|
+
assert.deepEqual(await claimed(5000), []);
|
|
637
|
+
});
|
|
638
|
+
it('replays a crashed post and fences its stale settlement after a new lease', async () => {
|
|
639
|
+
await store.addWebhook(hook('w1', 'ada@a.test', { verified: true }), 2);
|
|
640
|
+
await store.wakeWebhooks('ada@a.test', at());
|
|
641
|
+
assert.equal((await store.claimWebhooks(at(), 1, at(1), lease('old', at(10)))).length, 1);
|
|
642
|
+
assert.deepEqual(await store.claimWebhooks(at(9), 1, at(10), lease('early', at(20))), []);
|
|
643
|
+
assert.equal((await store.claimWebhooks(at(10), 1, at(11), lease('new', at(20)))).length, 1);
|
|
644
|
+
assert.equal(await store.settleWebhook('w1', at(), at(HOUR), 'old'), false);
|
|
645
|
+
await store.wakeWebhooks('ada@a.test', at(12));
|
|
646
|
+
assert.equal(await store.settleWebhook('w1', null, null, 'new'), true);
|
|
647
|
+
assert.equal((await store.claimWebhooks(at(12), 1, at(13), lease('wake', at(22)))).length, 1);
|
|
648
|
+
});
|
|
649
|
+
it('disables a permanently failing webhook only for the current claim token', async () => {
|
|
650
|
+
await store.addWebhook(hook('w1', 'ada@a.test', { verified: true }), 1);
|
|
651
|
+
await store.wakeWebhooks('ada@a.test', at());
|
|
652
|
+
await store.claimWebhooks(at(), 1, at(1), lease('old', at(10)));
|
|
653
|
+
await store.claimWebhooks(at(10), 1, at(11), lease('new', at(20)));
|
|
654
|
+
assert.equal(await store.settleWebhook('w1', at(), null, 'old', true), false);
|
|
655
|
+
assert.ok(await store.webhook('w1'));
|
|
656
|
+
assert.equal(await store.settleWebhook('w1', at(), null, 'new', true), true);
|
|
657
|
+
assert.equal(await store.webhook('w1'), null);
|
|
658
|
+
});
|
|
659
|
+
it('keeps when a webhook began failing, and a retry, unless a post is due anyway', async () => {
|
|
660
|
+
await store.addWebhook(hook('w1'), 5);
|
|
661
|
+
await store.verifyWebhook('w1');
|
|
662
|
+
await store.wakeWebhooks('ada@a.test', at());
|
|
663
|
+
await claimed(0);
|
|
664
|
+
await store.settleWebhook('w1', at(), at(60_000), 'w0');
|
|
665
|
+
assert.equal((await store.webhook('w1'))?.failingSince, at());
|
|
666
|
+
assert.deepEqual(await claimed(59_999), []);
|
|
667
|
+
assert.deepEqual(await claimed(60_000), ['w1']);
|
|
668
|
+
await store.wakeWebhooks('ada@a.test', at(61_000));
|
|
669
|
+
await store.settleWebhook('w1', at(), at(HOUR), 'w60000');
|
|
670
|
+
assert.deepEqual(await claimed(61_000), ['w1'], 'the wake stands');
|
|
671
|
+
await store.settleWebhook('w1', null, null, 'w61000');
|
|
672
|
+
assert.equal((await store.webhook('w1'))?.failingSince, null);
|
|
673
|
+
assert.deepEqual(await claimed(2 * HOUR), []);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
describe('deliveries', () => {
|
|
677
|
+
it('claims due rows earliest first, skipping servers, and never the same row twice', async () => {
|
|
678
|
+
const m = message('ada@a.test', ['bob@b.test']);
|
|
679
|
+
await store.putMessage(m, {
|
|
680
|
+
deliveries: [
|
|
681
|
+
delivery(m.id, 'b.test', { nextAttemptAt: at(2) }),
|
|
682
|
+
delivery(m.id, 'c.test', { nextAttemptAt: at(1), server: 'https://c.test' }),
|
|
683
|
+
delivery(m.id, 'd.test', { nextAttemptAt: at(HOUR) }),
|
|
684
|
+
],
|
|
685
|
+
});
|
|
686
|
+
assert.deepEqual((await store.claimDeliveries(at(10), 10, ['https://c.test'], lease('t1'))).map(d => d.domain), ['b.test']);
|
|
687
|
+
const racing = await Promise.all(['t2', 't3', 't4'].map(t => store.claimDeliveries(at(10), 10, [], lease(t))));
|
|
688
|
+
assert.deepEqual(racing.flat().map(d => d.domain), ['c.test'], 'b.test is leased, d.test not due, c.test claimed once');
|
|
689
|
+
const expired = await store.claimDeliveries(at(HOUR), 10, [], lease('t5', at(HOUR + 60_000)));
|
|
690
|
+
assert.deepEqual(expired.map(d => d.domain).sort(), ['b.test', 'c.test', 'd.test'], 'a lease that ran out frees its row');
|
|
691
|
+
assert.ok(expired.every(d => d.nextAttemptAt === at(HOUR + 60_000)));
|
|
692
|
+
assert.deepEqual(await store.claimDeliveries(at(HOUR), 10, [], lease('t6')), []);
|
|
693
|
+
});
|
|
694
|
+
it('settles a row only for the lease that claimed it, and never after it was delivered', async () => {
|
|
695
|
+
const m = message('ada@a.test', ['bob@b.test']);
|
|
696
|
+
await store.putMessage(m, { deliveries: [delivery(m.id, 'b.test')] });
|
|
697
|
+
const [first] = await store.claimDeliveries(at(), 1, [], lease('a', at(10)));
|
|
698
|
+
const [second] = await store.claimDeliveries(at(10), 1, [], lease('b', at(20)));
|
|
699
|
+
assert.equal(await store.settleDelivery({ ...first, status: 'delivered' }, 'a'), false, 'a stale lease');
|
|
700
|
+
const delivered = { ...second, status: 'delivered', server: 'https://b.test', attempts: 1, hints: {} };
|
|
701
|
+
assert.equal(await store.settleDelivery(delivered, 'b'), true);
|
|
702
|
+
assert.equal(await store.settleDelivery({ ...second, reason: 'late' }, 'b'), false, 'delivered stays');
|
|
703
|
+
const [row] = await store.deliveries(m.id);
|
|
704
|
+
assert.deepEqual([row.status, row.server, row.attempts, row.reason], ['delivered', 'https://b.test', 1, null]);
|
|
705
|
+
assert.deepEqual(await store.claimDeliveries(at(HOUR), 1, [], lease('c')), []);
|
|
706
|
+
});
|
|
707
|
+
it('clears the hints of a delivery once it ends, and keeps them while it is pending', async () => {
|
|
708
|
+
const m = message('ada@a.test', ['bob@b.test']);
|
|
709
|
+
const hints = { 'bob@b.test': { invite: 'x' } };
|
|
710
|
+
await store.putMessage(m, { deliveries: [delivery(m.id, 'b.test', { hints })] });
|
|
711
|
+
const [row] = await store.claimDeliveries(at(), 1, [], lease('a'));
|
|
712
|
+
await store.settleDelivery({ ...row, reason: 'temporary', nextAttemptAt: at(1) }, 'a');
|
|
713
|
+
assert.deepEqual((await store.deliveries(m.id))[0].hints, hints);
|
|
714
|
+
const [again] = await store.claimDeliveries(at(1), 1, [], lease('b'));
|
|
715
|
+
await store.settleDelivery({ ...again, status: 'failed', reason: 'expired' }, 'b');
|
|
716
|
+
assert.deepEqual((await store.deliveries(m.id))[0].hints, {});
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
describe('published messages, relations and correspondence', () => {
|
|
720
|
+
it('keeps a same-author retraction despite expiry, arrival order and another author', async () => {
|
|
721
|
+
const reply = message('bob@b.test', ['ada@a.test'], {
|
|
722
|
+
inReplyTo: sha('e'),
|
|
723
|
+
headers: { rel: 'reply', listed: 'yes' },
|
|
724
|
+
});
|
|
725
|
+
const retract = {
|
|
726
|
+
...message('bob@b.test', ['ada@a.test'], { inReplyTo: reply.id, headers: { rel: 'retract' } }),
|
|
727
|
+
expiresAt: at(10),
|
|
728
|
+
};
|
|
729
|
+
await store.putMessage(retract, { arrivals: [arrival(retract.id, 'ada@a.test')] });
|
|
730
|
+
// The original may arrive only after its valid retraction has already expired.
|
|
731
|
+
await store.sweep(at(10), at());
|
|
732
|
+
await store.putMessage(reply, { arrivals: [arrival(reply.id, 'ada@a.test')] });
|
|
733
|
+
assert.equal(await store.listingRetracted('ada@a.test', reply.id, reply.message.from), true);
|
|
734
|
+
assert.equal(await store.listingRetracted('cat@a.test', reply.id, reply.message.from), false);
|
|
735
|
+
assert.equal(await store.listingRetracted('ada@a.test', reply.id, 'eve@e.test'), false);
|
|
736
|
+
const publicReply = message('bob@b.test', [], { headers: { rel: 'reply', listed: 'yes' } });
|
|
737
|
+
const publicRetract = message('bob@b.test', [], { inReplyTo: publicReply.id, headers: { rel: 'retract' } });
|
|
738
|
+
await store.putMessage(publicRetract);
|
|
739
|
+
await store.putMessage(publicReply);
|
|
740
|
+
assert.equal(await store.listingRetracted('ada@a.test', publicReply.id, 'bob@b.test'), true);
|
|
741
|
+
await store.deleteMessage(reply.id);
|
|
742
|
+
assert.equal(await store.listingRetracted('ada@a.test', reply.id, 'bob@b.test'), true, 'an accepted early retraction remains durable');
|
|
743
|
+
});
|
|
744
|
+
it('keeps a reaccepted target suppressed after its prior incarnation was deleted', async () => {
|
|
745
|
+
const post = message('ada@a.test', [], { headers: { feed: 'posts' } });
|
|
746
|
+
const reply = message('bob@b.test', ['ada@a.test'], {
|
|
747
|
+
inReplyTo: post.id,
|
|
748
|
+
headers: { rel: 'reply', listed: 'yes' },
|
|
749
|
+
});
|
|
750
|
+
const retract = message('bob@b.test', ['ada@a.test'], { inReplyTo: reply.id, headers: { rel: 'retract' } });
|
|
751
|
+
await store.putMessage(post);
|
|
752
|
+
await store.putMessage(reply);
|
|
753
|
+
await store.addToInbox('ada@a.test', reply.id, at());
|
|
754
|
+
await store.putMessage(retract);
|
|
755
|
+
await store.deleteMessage(reply.id);
|
|
756
|
+
assert.deepEqual((await store.repliesTo('ada@a.test', post.id, null, 10)).records, []);
|
|
757
|
+
await store.putMessage(reply);
|
|
758
|
+
await store.addToInbox('ada@a.test', reply.id, at(1));
|
|
759
|
+
assert.deepEqual(await store.message(reply.id), reply, 'the original target exists again');
|
|
760
|
+
const candidates = (await store.repliesTo('ada@a.test', post.id, null, 10)).records;
|
|
761
|
+
assert.deepEqual(candidates.map(r => r.message.id), [reply.id], 'the new target incarnation is a real relation candidate');
|
|
762
|
+
const visible = [];
|
|
763
|
+
for (const r of candidates)
|
|
764
|
+
if (!(await store.listingRetracted('ada@a.test', r.message.id, r.message.message.from)))
|
|
765
|
+
visible.push(r.message.id);
|
|
766
|
+
assert.deepEqual(visible, [], 'the durable same-author suppression still removes the reaccepted target');
|
|
767
|
+
});
|
|
768
|
+
it('reports the first kept retraction after inbox admission, with no repeated arrival work', async () => {
|
|
769
|
+
const m = message('bob@b.test', ['ada@a.test'], { inReplyTo: sha('a'), headers: { rel: 'retract' } });
|
|
770
|
+
await store.putMessage(m, { arrivals: [arrival(m.id, 'ada@a.test')] });
|
|
771
|
+
await store.addToInbox('ada@a.test', m.id, at());
|
|
772
|
+
assert.equal(await store.arrivalDone(m.id, 'ada@a.test', true), 'kept');
|
|
773
|
+
assert.equal(await store.arrivalDone(m.id, 'ada@a.test', true), null);
|
|
774
|
+
assert.equal((await store.arrivals(at(), 10)).length, 0);
|
|
775
|
+
assert.ok(await store.message(m.id));
|
|
776
|
+
});
|
|
777
|
+
it('keeps the claims a carrier signed for a message, beside the ones judged here', async () => {
|
|
778
|
+
const signedClaims = { claims: [{ type: 'acts-for', agent: 'ada@a.test' }], signature: 'AAAA' };
|
|
779
|
+
const m = { ...message('ada@a.test', ['bob@b.test']), signedClaims };
|
|
780
|
+
await store.putMessage(m);
|
|
781
|
+
await store.addToInbox('bob@b.test', m.id, at());
|
|
782
|
+
assert.deepEqual((await store.message(m.id)).signedClaims, signedClaims);
|
|
783
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 10, at())).messages[0].signedClaims, signedClaims);
|
|
784
|
+
assert.equal((await store.message(message('ada@a.test', []).id))?.signedClaims ?? null, null);
|
|
785
|
+
});
|
|
786
|
+
it("pages an agent's published messages newest first, by opaque cursor", async () => {
|
|
787
|
+
const posts = [1, 2, 3].map(() => message('ada@a.test', []));
|
|
788
|
+
for (const m of [...posts, message('ada@a.test', ['bob@b.test']), message('cat@a.test', [])]) {
|
|
789
|
+
await store.putMessage(m);
|
|
790
|
+
}
|
|
791
|
+
const first = await store.published('ada@a.test', null, 2);
|
|
792
|
+
assert.deepEqual(first.records.map(r => r.message.id), [posts[2].id, posts[1].id]);
|
|
793
|
+
assert.equal(first.more, true);
|
|
794
|
+
assert.ok(first.records.every(r => /^[\x21-\x7e]{1,128}$/.test(r.cursor)));
|
|
795
|
+
const rest = await store.published('ada@a.test', first.records[1].cursor, 2);
|
|
796
|
+
assert.deepEqual(rest.records.map(r => r.message.id), [posts[0].id]);
|
|
797
|
+
assert.equal(rest.more, false);
|
|
798
|
+
assert.deepEqual((await store.published('eve@a.test', null, 2)).records, []);
|
|
799
|
+
});
|
|
800
|
+
it("pages the messages in an agent's inbox that point at one message, newest first", async () => {
|
|
801
|
+
const post = message('ada@a.test', []);
|
|
802
|
+
const [c1, c2, other] = [post.id, post.id, sha('9')].map(to => message('bob@b.test', ['ada@a.test'], { inReplyTo: to }));
|
|
803
|
+
for (const m of [post, c1, c2, other])
|
|
804
|
+
await store.putMessage(m);
|
|
805
|
+
for (const m of [c1, c2, other])
|
|
806
|
+
await store.addToInbox('ada@a.test', m.id, at());
|
|
807
|
+
const page = await store.repliesTo('ada@a.test', post.id, null, 1);
|
|
808
|
+
assert.deepEqual(page.records.map(r => r.message.id), [c2.id]);
|
|
809
|
+
assert.equal(page.more, true);
|
|
810
|
+
const next = await store.repliesTo('ada@a.test', post.id, page.records[0].cursor, 5);
|
|
811
|
+
assert.deepEqual(next.records.map(r => r.message.id), [c1.id]);
|
|
812
|
+
assert.deepEqual((await store.repliesTo('cat@a.test', post.id, null, 5)).records, []);
|
|
813
|
+
});
|
|
814
|
+
it('says whether a message is in an inbox, and whether an agent here wrote to an address', async () => {
|
|
815
|
+
const sent = message('ada@a.test', ['bob@b.test', 'cat@c.test']);
|
|
816
|
+
const received = { ...message('bob@b.test', ['ada@a.test']), carrier: 'https://b.test' };
|
|
817
|
+
for (const m of [sent, received])
|
|
818
|
+
await store.putMessage(m);
|
|
819
|
+
await store.addToInbox('ada@a.test', received.id, at());
|
|
820
|
+
assert.equal(await store.inInbox('ada@a.test', received.id), true);
|
|
821
|
+
assert.equal(await store.inInbox('ada@a.test', sent.id), false);
|
|
822
|
+
assert.equal(await store.wroteTo('ada@a.test', 'cat@c.test', SELF), true);
|
|
823
|
+
assert.equal(await store.wroteTo('ada@a.test', 'eve@e.test', SELF), false);
|
|
824
|
+
assert.equal(await store.wroteTo('bob@b.test', 'ada@a.test', SELF), false, 'written elsewhere');
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
describe('parts', () => {
|
|
828
|
+
it('keeps bytes per source, never shared between sources', async () => {
|
|
829
|
+
await store.putPart(sha('a'), Buffer.from('mine'), { uploader: 'ada@a.test' }, at());
|
|
830
|
+
await store.putPart(sha('a'), Buffer.from('theirs'), { carrier: 'https://e.test' }, at());
|
|
831
|
+
assert.deepEqual(await store.part(sha('a'), { uploader: 'ada@a.test' }), Buffer.from('mine'));
|
|
832
|
+
assert.deepEqual(await store.part(sha('a'), { carrier: 'https://e.test' }), Buffer.from('theirs'));
|
|
833
|
+
assert.equal(await store.part(sha('a'), { uploader: 'cat@a.test' }), null);
|
|
834
|
+
assert.equal(await store.part(sha('a'), { carrier: 'https://f.test' }), null);
|
|
835
|
+
});
|
|
836
|
+
it('says which bytes are held for an agent, and whose messages name a part for a reader', async () => {
|
|
837
|
+
const part = { type: 'application/pdf', sha256: sha('a'), size: 3 };
|
|
838
|
+
for (const a of ['ada@a.test', 'old@a.test', 'cat@a.test'])
|
|
839
|
+
await store.createAgent(agent(a, `k-${a}`));
|
|
840
|
+
const pub = message('ada@a.test', [], { parts: [part] });
|
|
841
|
+
const retiredPub = message('old@a.test', [], { parts: [part] });
|
|
842
|
+
const priv = message('ada@a.test', ['bob@b.test'], { parts: [part] });
|
|
843
|
+
const received = { ...message('eve@e.test', ['cat@a.test'], { parts: [part] }), carrier: 'https://e.test' };
|
|
844
|
+
const local = message('ada@a.test', ['cat@a.test'], { parts: [part] });
|
|
845
|
+
const forwarded = message('cat@a.test', ['bob@b.test'], { parts: [part] });
|
|
846
|
+
for (const m of [pub, retiredPub, priv, received, local, forwarded])
|
|
847
|
+
await store.putMessage(m);
|
|
848
|
+
await store.retireAgent('old@a.test', at());
|
|
849
|
+
await store.addToInbox('cat@a.test', received.id, at());
|
|
850
|
+
await store.addToInbox('cat@a.test', local.id, at());
|
|
851
|
+
const sent = (m) => [
|
|
852
|
+
delivery(m.id, 'b.test', { status: 'delivered', server: 'https://b.test' }),
|
|
853
|
+
];
|
|
854
|
+
await store.putMessage(priv, { deliveries: sent(priv) });
|
|
855
|
+
await store.putMessage(forwarded, { deliveries: sent(forwarded) });
|
|
856
|
+
const sorted = (xs) => xs.sort((x, y) => JSON.stringify(x).localeCompare(JSON.stringify(y)));
|
|
857
|
+
assert.deepEqual(sorted(await store.heldFor(sha('a'), 'cat@a.test', SELF)), [
|
|
858
|
+
{ carrier: 'https://e.test', size: 3 },
|
|
859
|
+
{ uploader: 'ada@a.test', size: 3 },
|
|
860
|
+
]);
|
|
861
|
+
assert.deepEqual(await store.heldFor(sha('a'), 'eve@a.test', SELF), []);
|
|
862
|
+
const forward = message('cat@a.test', ['dot@a.test'], { parts: [part] });
|
|
863
|
+
await store.putMessage(forward);
|
|
864
|
+
await store.addToInbox('dot@a.test', forward.id, at());
|
|
865
|
+
assert.deepEqual(sorted(await store.heldFor(sha('a'), 'dot@a.test', SELF)), [
|
|
866
|
+
{ carrier: 'https://e.test', size: 3 },
|
|
867
|
+
{ uploader: 'ada@a.test', size: 3 },
|
|
868
|
+
{ uploader: 'cat@a.test', size: 3 },
|
|
869
|
+
], 'a forward from an agent here: its upload, and what is held for it');
|
|
870
|
+
const again = message('dot@a.test', ['fay@a.test'], { parts: [part] });
|
|
871
|
+
await store.putMessage(again);
|
|
872
|
+
await store.addToInbox('fay@a.test', again.id, at());
|
|
873
|
+
const back = message('fay@a.test', ['cat@a.test'], { parts: [part] });
|
|
874
|
+
await store.putMessage(back);
|
|
875
|
+
await store.addToInbox('cat@a.test', back.id, at());
|
|
876
|
+
assert.deepEqual(sorted(await store.heldFor(sha('a'), 'fay@a.test', SELF)), [
|
|
877
|
+
{ carrier: 'https://e.test', size: 3 },
|
|
878
|
+
{ uploader: 'ada@a.test', size: 3 },
|
|
879
|
+
{ uploader: 'cat@a.test', size: 3 },
|
|
880
|
+
{ uploader: 'dot@a.test', size: 3 },
|
|
881
|
+
{ uploader: 'fay@a.test', size: 3 },
|
|
882
|
+
], 'forwarded again: what was held for every agent here that passed it on, each once, through a cycle');
|
|
883
|
+
await store.putPart(sha('c'), Buffer.from('abc'), { uploader: 'eve@a.test' }, at());
|
|
884
|
+
assert.deepEqual(await store.heldFor(sha('c'), 'eve@a.test', SELF), [{ uploader: 'eve@a.test', size: 3 }]);
|
|
885
|
+
const authors = (xs) => xs.map(x => x.author).sort();
|
|
886
|
+
assert.deepEqual(authors(await store.partNamers(sha('a'), { published: true }, SELF)), ['ada@a.test']);
|
|
887
|
+
const [named] = await store.partNamers(sha('a'), { published: true }, SELF);
|
|
888
|
+
assert.deepEqual(named.message, pub.message);
|
|
889
|
+
assert.deepEqual(authors(await store.partNamers(sha('a'), { server: 'https://b.test' }, SELF)), [
|
|
890
|
+
'ada@a.test',
|
|
891
|
+
'cat@a.test',
|
|
892
|
+
]);
|
|
893
|
+
assert.deepEqual(await store.partNamers(sha('a'), { server: 'https://e.test' }, SELF), []);
|
|
894
|
+
assert.deepEqual(await store.partNamers(sha('b'), { published: true }, SELF), []);
|
|
895
|
+
});
|
|
896
|
+
it('follows repeated local forwards, terminates cycles and isolates unrelated sources', async () => {
|
|
897
|
+
const part = { type: 'application/octet-stream', sha256: sha('d'), size: 3 };
|
|
898
|
+
await store.putPart(sha('d'), Buffer.from('abc'), { uploader: 'a@a.test' }, at());
|
|
899
|
+
await store.putPart(sha('d'), Buffer.from('abc'), { uploader: 'unrelated@a.test' }, at());
|
|
900
|
+
const unrelated = message('elsewhere@e.test', ['unrelated@a.test'], { parts: [part] });
|
|
901
|
+
await store.putMessage({ ...unrelated, carrier: 'https://unrelated.test' });
|
|
902
|
+
await store.addToInbox('unrelated@a.test', unrelated.id, at());
|
|
903
|
+
for (const [from, to] of [
|
|
904
|
+
['a@a.test', 'b@a.test'],
|
|
905
|
+
['b@a.test', 'c@a.test'],
|
|
906
|
+
['c@a.test', 'd@a.test'],
|
|
907
|
+
['d@a.test', 'b@a.test'],
|
|
908
|
+
]) {
|
|
909
|
+
const m = message(from, [to], { parts: [part] });
|
|
910
|
+
await store.putMessage(m);
|
|
911
|
+
await store.addToInbox(to, m.id, at());
|
|
912
|
+
}
|
|
913
|
+
const sources = await store.heldFor(sha('d'), 'd@a.test', SELF);
|
|
914
|
+
assert.deepEqual(sources.map(s => ('uploader' in s ? s.uploader : s.carrier)).sort(), [
|
|
915
|
+
'a@a.test',
|
|
916
|
+
'b@a.test',
|
|
917
|
+
'c@a.test',
|
|
918
|
+
'd@a.test',
|
|
919
|
+
]);
|
|
920
|
+
assert.ok(sources.every(s => s.size === 3));
|
|
921
|
+
assert.equal(sources.length, 4, 'each source appears once despite the cycle');
|
|
922
|
+
assert.deepEqual(await store.heldFor(sha('d'), 'no-grant@a.test', SELF), []);
|
|
923
|
+
assert.deepEqual(await store.heldFor(sha('e'), 'd@a.test', SELF), []);
|
|
924
|
+
});
|
|
925
|
+
it('queues one fetch per part and carrier, fairly and with token-fenced settlements', async () => {
|
|
926
|
+
const f = await store.queueFetch({
|
|
927
|
+
sha256: sha('a'),
|
|
928
|
+
carrier: 'https://e.test',
|
|
929
|
+
size: 3,
|
|
930
|
+
attempts: 0,
|
|
931
|
+
dueAt: at(10),
|
|
932
|
+
});
|
|
933
|
+
assert.deepEqual(await store.queueFetch(f), f);
|
|
934
|
+
assert.deepEqual(await store.queueFetch({ ...f, dueAt: at(1) }), f);
|
|
935
|
+
for (const [hash, due] of [
|
|
936
|
+
['b', 5],
|
|
937
|
+
['c', 6],
|
|
938
|
+
['d', 7],
|
|
939
|
+
])
|
|
940
|
+
await store.queueFetch({ ...f, sha256: sha(hash), carrier: 'https://f.test', dueAt: at(due) });
|
|
941
|
+
assert.deepEqual((await store.fetches(at(10), 10)).map(x => x.sha256.slice(0, 1)), ['b', 'a', 'c', 'd']);
|
|
942
|
+
assert.equal(await store.claimFetch(f, at(5), lease('early')), false);
|
|
943
|
+
const won = await Promise.all([1, 2, 3].map(i => store.claimFetch(f, at(10), lease(`t${i}`, at(HOUR)))));
|
|
944
|
+
assert.equal(won.filter(Boolean).length, 1);
|
|
945
|
+
const token = `t${won.indexOf(true) + 1}`;
|
|
946
|
+
assert.equal(await store.renewFetch(f.sha256, f.carrier, 'wrong', at(11), at(2 * HOUR)), false);
|
|
947
|
+
assert.equal(await store.renewFetch(f.sha256, f.carrier, token, at(11), at(2 * HOUR)), true);
|
|
948
|
+
assert.equal(await store.fetchRetry(f.sha256, f.carrier, at(20), 'wrong'), false);
|
|
949
|
+
assert.equal(await store.fetchRetry(f.sha256, f.carrier, at(20), token), true);
|
|
950
|
+
const row = (await store.fetches(at(20), 10)).find(r => r.carrier === f.carrier);
|
|
951
|
+
assert.equal(row.attempts, 1);
|
|
952
|
+
assert.equal(await store.claimFetch(f, at(20), lease('stale')), false);
|
|
953
|
+
assert.equal(await store.claimFetch(row, at(20), lease('new', at(30))), true);
|
|
954
|
+
assert.equal(await store.fetchDone(f.sha256, f.carrier, token), false);
|
|
955
|
+
assert.equal(await store.fetchGiveUp(f.sha256, f.carrier, token, at(HOUR)), false);
|
|
956
|
+
assert.equal(await store.fetchDefer(f.sha256, f.carrier, at(40), 'new'), true);
|
|
957
|
+
const deferred = (await store.fetches(at(40), 10)).find(r => r.carrier === f.carrier);
|
|
958
|
+
assert.equal(deferred.attempts, 1);
|
|
959
|
+
assert.equal(await store.claimFetch(deferred, at(40), lease('last', at(50))), true);
|
|
960
|
+
assert.equal(await store.fetchGiveUp(f.sha256, f.carrier, 'last', at(HOUR)), true);
|
|
961
|
+
assert.equal(await store.fetchExhausted(f.sha256, f.carrier, at(50)), true);
|
|
962
|
+
assert.ok((await store.fetches(at(2 * HOUR), 10)).every(r => r.carrier !== f.carrier));
|
|
963
|
+
assert.equal(await store.fetchExhausted(f.sha256, f.carrier, at(HOUR)), false);
|
|
964
|
+
const next = await store.queueFetch({ ...f, dueAt: at(HOUR) }, at(HOUR));
|
|
965
|
+
assert.equal(next.revision, 0);
|
|
966
|
+
assert.notEqual(next.generation, f.generation);
|
|
967
|
+
assert.equal(next.dueAt, at(HOUR));
|
|
968
|
+
});
|
|
969
|
+
it('rejects a read snapshot after a defer restores the same attempts and due time', async () => {
|
|
970
|
+
const f = { sha256: sha('e'), carrier: 'https://e.test', size: 3, attempts: 0, dueAt: at() };
|
|
971
|
+
const snapshot = await store.queueFetch(f);
|
|
972
|
+
assert.equal(await store.claimFetch(snapshot, at(), lease('a', at(10))), true);
|
|
973
|
+
assert.equal(await store.fetchDefer(f.sha256, f.carrier, at(), 'a'), true);
|
|
974
|
+
assert.equal(await store.claimFetch(snapshot, at(), lease('stale', at(10))), false);
|
|
975
|
+
const [current] = await store.fetches(at(), 1);
|
|
976
|
+
assert.equal(await store.claimFetch(current, at(), lease('new', at(10))), true);
|
|
977
|
+
});
|
|
978
|
+
it('rejects a stale snapshot after its completed row is recreated', async () => {
|
|
979
|
+
const seed = { sha256: sha('f'), carrier: 'https://e.test', size: 3, attempts: 0, dueAt: at() };
|
|
980
|
+
const old = await store.queueFetch(seed);
|
|
981
|
+
assert.equal(await store.claimFetch(old, at(), lease('a', at(10))), true);
|
|
982
|
+
assert.equal(await store.fetchDone(seed.sha256, seed.carrier, 'a'), true);
|
|
983
|
+
const fresh = await store.queueFetch(seed);
|
|
984
|
+
assert.notEqual(fresh.generation, old.generation);
|
|
985
|
+
assert.equal(await store.claimFetch(old, at(), lease('stale', at(10))), false);
|
|
986
|
+
assert.equal(await store.claimFetch(fresh, at(), lease('new', at(10))), true);
|
|
987
|
+
});
|
|
988
|
+
it('fences an expired fetch owner after a new worker claims it, and cannot renew after expiry', async () => {
|
|
989
|
+
const f = await store.queueFetch({
|
|
990
|
+
sha256: sha('b'),
|
|
991
|
+
carrier: 'https://e.test',
|
|
992
|
+
size: 3,
|
|
993
|
+
attempts: 0,
|
|
994
|
+
dueAt: at(),
|
|
995
|
+
});
|
|
996
|
+
assert.equal(await store.claimFetch(f, at(), lease('a', at(10))), true);
|
|
997
|
+
assert.equal(await store.renewFetch(f.sha256, f.carrier, 'a', at(10), at(20)), false);
|
|
998
|
+
const [row] = await store.fetches(at(10), 1);
|
|
999
|
+
assert.equal(await store.claimFetch(row, at(10), lease('b', at(30))), true);
|
|
1000
|
+
assert.equal(await store.fetchDone(f.sha256, f.carrier, 'a'), false);
|
|
1001
|
+
assert.equal(await store.fetchRetry(f.sha256, f.carrier, at(50), 'a'), false);
|
|
1002
|
+
assert.equal(await store.fetchDefer(f.sha256, f.carrier, at(50), 'a'), false);
|
|
1003
|
+
assert.equal(await store.fetchGiveUp(f.sha256, f.carrier, 'a', at(HOUR)), false);
|
|
1004
|
+
assert.equal(await store.fetchDone(f.sha256, f.carrier, 'b'), true);
|
|
1005
|
+
assert.deepEqual(await store.fetches(at(HOUR), 10), []);
|
|
1006
|
+
});
|
|
1007
|
+
it('queues no more than `max` waiting fetches for a carrier, still answering one already queued', async () => {
|
|
1008
|
+
const f = { sha256: sha('a'), carrier: 'https://e.test', size: 3, attempts: 0, dueAt: at() };
|
|
1009
|
+
assert.ok(await store.queueFetch(f, at(), 2));
|
|
1010
|
+
const second = await store.queueFetch({ ...f, sha256: sha('b') }, at(), 2);
|
|
1011
|
+
assert.ok(second);
|
|
1012
|
+
assert.equal(await store.queueFetch({ ...f, sha256: sha('c') }, at(), 2), null, 'its carrier has two waiting');
|
|
1013
|
+
assert.deepEqual(await store.queueFetch({ ...f, sha256: sha('b') }, at(), 2), second, 'one queued is answered');
|
|
1014
|
+
assert.ok(await store.queueFetch({ ...f, carrier: 'https://f.test', sha256: sha('c') }, at(), 2), 'per carrier');
|
|
1015
|
+
assert.equal(await store.claimFetch(second, at(), lease('t', at(10))), true);
|
|
1016
|
+
assert.equal(await store.fetchGiveUp(sha('b'), f.carrier, 't', at(HOUR)), true);
|
|
1017
|
+
assert.ok(await store.queueFetch({ ...f, sha256: sha('c') }, at(), 2), 'a fetch given up waits no more');
|
|
1018
|
+
assert.deepEqual((await store.fetches(at(), 10)).map(r => r.sha256.slice(0, 1)).sort(), ['a', 'c', 'c']);
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
describe('marks, counters and values', () => {
|
|
1022
|
+
it('lets exactly one of racing callers set a mark, until it expires', async () => {
|
|
1023
|
+
const won = await Promise.all([1, 2, 3, 4].map(() => store.mark('nonce', 'x', at(600_000), at())));
|
|
1024
|
+
assert.equal(won.filter(Boolean).length, 1);
|
|
1025
|
+
assert.equal(await store.marked('nonce', 'x', at(599_999)), true);
|
|
1026
|
+
assert.equal(await store.marked('nonce', 'x', at(600_000)), false);
|
|
1027
|
+
assert.equal(await store.mark('nonce', 'x', at(1_200_000), at(600_000)), true);
|
|
1028
|
+
assert.equal(await store.marked('other', 'x', at()), false);
|
|
1029
|
+
});
|
|
1030
|
+
it('counts atomically, starting over once the count expires', async () => {
|
|
1031
|
+
const counts = await Promise.all([1, 2, 3].map(() => store.count('c', 'x', at(1000), at())));
|
|
1032
|
+
assert.deepEqual(counts.sort(), [1, 2, 3]);
|
|
1033
|
+
assert.equal(await store.count('c', 'x', at(5000), at(1000)), 1);
|
|
1034
|
+
const added = await Promise.all([500, 700].map(by => store.count('b', 'x', at(1000), at(), by)));
|
|
1035
|
+
assert.equal(Math.max(...added), 1200, 'by any amount, atomically');
|
|
1036
|
+
assert.equal(await store.count('b', 'x', at(1000), at(), -200), 1000, 'taken back by a negative one');
|
|
1037
|
+
});
|
|
1038
|
+
it('keeps values until they expire, and valueOnce keeps the first', async () => {
|
|
1039
|
+
await store.setValue('v', 'x', { a: 1 }, at(1000));
|
|
1040
|
+
assert.deepEqual(await store.value('v', 'x', at(999)), { a: 1 });
|
|
1041
|
+
assert.equal(await store.value('v', 'x', at(1000)), null);
|
|
1042
|
+
assert.equal(await store.valueOnce('v', 'y', 'first', null, at()), 'first');
|
|
1043
|
+
assert.equal(await store.valueOnce('v', 'y', 'second', null, at()), 'first');
|
|
1044
|
+
assert.equal(await store.releaseValue('v', 'y', 'second'), false, 'released only while it is that value');
|
|
1045
|
+
assert.equal(await store.value('v', 'y', at()), 'first');
|
|
1046
|
+
assert.equal(await store.releaseValue('v', 'y', 'first'), true);
|
|
1047
|
+
assert.equal(await store.valueOnce('v', 'y', 'again', null, at()), 'again', 'and free once released');
|
|
1048
|
+
// Renewed only while it is the same value, so a lease another caller took is never extended.
|
|
1049
|
+
await store.valueOnce('v', 'z', 'mine', at(1000), at());
|
|
1050
|
+
assert.equal(await store.renewValue('v', 'z', 'theirs', at(5000)), false);
|
|
1051
|
+
assert.equal(await store.renewValue('v', 'z', 'mine', at(5000)), true);
|
|
1052
|
+
assert.equal(await store.value('v', 'z', at(4999)), 'mine', 'kept until the new expiry');
|
|
1053
|
+
assert.equal(await store.valueOnce('v', 'z', 'theirs', at(9000), at(2000)), 'mine', 'and not taken meanwhile');
|
|
1054
|
+
assert.equal(await store.value('v', 'z', at(5000)), null);
|
|
1055
|
+
await store.setValue('v', 'y', 'third', null);
|
|
1056
|
+
assert.equal(await store.value('v', 'y', at(10 * HOUR)), 'third');
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
describe('sweep', () => {
|
|
1060
|
+
it('removes fetches no message from their carrier names any more, waiting or given up', async () => {
|
|
1061
|
+
const named = { type: 'a/b', sha256: sha('a'), size: 1 };
|
|
1062
|
+
const fromE = { ...message('eve@e.test', ['ada@a.test'], { parts: [named] }), carrier: 'https://e.test' };
|
|
1063
|
+
const gone = {
|
|
1064
|
+
...message('eve@e.test', ['ada@a.test'], { parts: [{ ...named, sha256: sha('b') }] }),
|
|
1065
|
+
carrier: 'https://e.test',
|
|
1066
|
+
expiresAt: at(HOUR),
|
|
1067
|
+
};
|
|
1068
|
+
await store.putMessage(fromE);
|
|
1069
|
+
await store.putMessage(gone);
|
|
1070
|
+
const f = { carrier: 'https://e.test', size: 1, attempts: 0, dueAt: at(2 * HOUR) };
|
|
1071
|
+
await store.queueFetch({ ...f, sha256: sha('a') });
|
|
1072
|
+
await store.queueFetch({ ...f, sha256: sha('b') });
|
|
1073
|
+
await store.queueFetch({ ...f, sha256: sha('a'), carrier: 'https://f.test' }, undefined);
|
|
1074
|
+
const lost = await store.queueFetch({ ...f, sha256: sha('c'), dueAt: at() });
|
|
1075
|
+
assert.equal(await store.claimFetch(lost, at(), lease('t', at(10))), true);
|
|
1076
|
+
assert.equal(await store.fetchGiveUp(sha('c'), f.carrier, 't', at(48 * HOUR)), true);
|
|
1077
|
+
await store.sweep(at(HOUR), at(HOUR));
|
|
1078
|
+
assert.deepEqual((await store.fetches(at(3 * HOUR), 10)).map(r => `${r.sha256.slice(0, 1)} ${r.carrier}`), ['a https://e.test'], 'a message carried by e.test still names a; b went with its message; no message f.test carried names a');
|
|
1079
|
+
assert.equal(await store.fetchExhausted(sha('c'), f.carrier, at(2 * HOUR)), false, 'given up, then unnamed');
|
|
1080
|
+
});
|
|
1081
|
+
it('removes expired messages with their ties, unnamed bytes per source, and expired marks', async () => {
|
|
1082
|
+
const named = { type: 'a/b', sha256: sha('a'), size: 1 };
|
|
1083
|
+
const old = { ...message('ada@a.test', ['bob@b.test'], { parts: [named] }), expiresAt: at(HOUR) };
|
|
1084
|
+
const kept = { ...message('ada@a.test', ['bob@b.test']), expiresAt: at(48 * HOUR) };
|
|
1085
|
+
const fromE = { ...message('eve@e.test', ['ada@a.test'], { parts: [named] }), carrier: 'https://e.test' };
|
|
1086
|
+
await store.putMessage(old, { deliveries: [delivery(old.id, 'b.test')] });
|
|
1087
|
+
await store.putMessage(kept);
|
|
1088
|
+
await store.putMessage(fromE);
|
|
1089
|
+
await store.addToInbox('bob@b.test', old.id, at());
|
|
1090
|
+
await store.putPart(sha('a'), Buffer.from('a'), { uploader: 'ada@a.test' }, at());
|
|
1091
|
+
await store.putPart(sha('a'), Buffer.from('a'), { carrier: 'https://e.test' }, at());
|
|
1092
|
+
await store.putPart(sha('a'), Buffer.from('a'), { carrier: 'https://f.test' }, at());
|
|
1093
|
+
await store.putPart(sha('b'), Buffer.from('b'), { uploader: 'ada@a.test' }, at());
|
|
1094
|
+
await store.putPart(sha('c'), Buffer.from('c'), { uploader: 'ada@a.test' }, at(2 * HOUR));
|
|
1095
|
+
await store.mark('m', 'x', at(HOUR), at());
|
|
1096
|
+
const counts = await store.sweep(at(HOUR), at(HOUR));
|
|
1097
|
+
assert.equal(counts.messages, 1);
|
|
1098
|
+
assert.equal(await store.message(old.id), null);
|
|
1099
|
+
assert.ok(await store.message(kept.id));
|
|
1100
|
+
assert.deepEqual((await store.sync('bob@b.test', 0, 10, at())).messages, []);
|
|
1101
|
+
assert.deepEqual(ids(await store.sync('ada@a.test', 0, 10, at())), [kept.id]);
|
|
1102
|
+
assert.deepEqual(await store.deliveries(old.id), []);
|
|
1103
|
+
assert.equal(await store.part(sha('a'), { uploader: 'ada@a.test' }), null, 'no message by ada names it');
|
|
1104
|
+
assert.ok(await store.part(sha('a'), { carrier: 'https://e.test' }), 'a message carried by e.test names it');
|
|
1105
|
+
assert.equal(await store.part(sha('a'), { carrier: 'https://f.test' }), null, 'fetched bytes go at once');
|
|
1106
|
+
assert.equal(await store.part(sha('b'), { uploader: 'ada@a.test' }), null, 'an unnamed upload goes after its grace');
|
|
1107
|
+
assert.ok(await store.part(sha('c'), { uploader: 'ada@a.test' }), 'an upload within its grace stays');
|
|
1108
|
+
assert.equal(await store.marked('m', 'x', at()), false);
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
/** Host admission boundary tests; factories supply the same options to their adapter constructor. */
|
|
1114
|
+
export function retractionCapacityContract(name, make) {
|
|
1115
|
+
describe(`Retraction capacity: ${name}`, () => {
|
|
1116
|
+
const retract = (from = 'bob@b.test') => message(from, ['ada@a.test'], { inReplyTo: sha('a'), headers: { rel: 'retract' } });
|
|
1117
|
+
const capacity = (e) => !!e &&
|
|
1118
|
+
typeof e === 'object' &&
|
|
1119
|
+
e.code === 'store_capacity' &&
|
|
1120
|
+
e.message === 'The server is temporarily at capacity.';
|
|
1121
|
+
it('admits one last-slot writer and rejects before any new state becomes visible', async () => {
|
|
1122
|
+
const s = await make({ maxRecords: 1, maxPerAuthor: 1 });
|
|
1123
|
+
try {
|
|
1124
|
+
const records = [retract(), retract('cat@c.test')];
|
|
1125
|
+
const results = await Promise.allSettled(records.map(m => s.putMessage(m, { arrivals: [arrival(m.id, 'ada@a.test')] })));
|
|
1126
|
+
assert.equal(results.filter(r => r.status === 'fulfilled').length, 1);
|
|
1127
|
+
const rejected = results.findIndex(r => r.status === 'rejected');
|
|
1128
|
+
assert.ok(capacity(results[rejected].reason));
|
|
1129
|
+
assert.equal(await s.message(records[rejected].id), null);
|
|
1130
|
+
assert.equal(await s.listingRetracted('ada@a.test', sha('a'), records[rejected].message.from), false);
|
|
1131
|
+
assert.equal((await s.arrivals(at(), 10)).length, 1);
|
|
1132
|
+
}
|
|
1133
|
+
finally {
|
|
1134
|
+
await s.close();
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
it('bounds each author independently and never recycles an accepted deletion', async () => {
|
|
1138
|
+
const s = await make({ maxRecords: 2, maxPerAuthor: 1 });
|
|
1139
|
+
try {
|
|
1140
|
+
const first = retract(), repeat = { ...first, carrier: 'https://changed.test' };
|
|
1141
|
+
await s.putMessage(first);
|
|
1142
|
+
assert.deepEqual(await s.putMessage(repeat), first);
|
|
1143
|
+
await assert.rejects(s.putMessage(retract()), capacity);
|
|
1144
|
+
const other = retract('cat@c.test');
|
|
1145
|
+
await s.putMessage(other);
|
|
1146
|
+
await s.deleteMessage(first.id);
|
|
1147
|
+
assert.deepEqual(await s.putMessage(repeat, { arrivals: [arrival(first.id, 'ada@a.test')] }), first);
|
|
1148
|
+
assert.equal(await s.message(first.id), null);
|
|
1149
|
+
assert.equal(await s.listingRetracted('ada@a.test', sha('a'), 'bob@b.test'), true);
|
|
1150
|
+
await assert.rejects(s.putMessage(retract('eve@e.test')), capacity);
|
|
1151
|
+
assert.equal((await s.arrivals(at(), 10)).length, 0);
|
|
1152
|
+
}
|
|
1153
|
+
finally {
|
|
1154
|
+
await s.close();
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
it('charges UTF-8 bytes and additional journals before accepting a duplicate', async () => {
|
|
1158
|
+
const s = await make({ maxBytes: 8192 });
|
|
1159
|
+
try {
|
|
1160
|
+
const first = retract();
|
|
1161
|
+
await s.putMessage(first);
|
|
1162
|
+
const row = arrival(first.id, 'ada@a.test', { hints: { large: 'é'.repeat(5000) } });
|
|
1163
|
+
await assert.rejects(s.putMessage(first, { arrivals: [row] }), capacity);
|
|
1164
|
+
assert.deepEqual(await s.message(first.id), first);
|
|
1165
|
+
assert.equal((await s.arrivals(at(), 10)).length, 0);
|
|
1166
|
+
const large = retract('cat@c.test');
|
|
1167
|
+
large.claims = [{ large: 'é'.repeat(5000) }];
|
|
1168
|
+
await assert.rejects(s.putMessage(large), capacity);
|
|
1169
|
+
assert.equal(await s.message(large.id), null);
|
|
1170
|
+
assert.equal(await s.listingRetracted('ada@a.test', sha('a'), 'cat@c.test'), false);
|
|
1171
|
+
}
|
|
1172
|
+
finally {
|
|
1173
|
+
await s.close();
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
}
|