@nicknisi/pi-relay 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -6
- package/core.ts +314 -0
- package/dist/core.d.ts +32 -0
- package/dist/core.js +223 -0
- package/dist/core.test.d.ts +1 -0
- package/dist/core.test.js +358 -0
- package/dist/filesystem.d.ts +42 -0
- package/dist/filesystem.js +614 -0
- package/dist/format.d.ts +3 -1
- package/dist/format.js +13 -0
- package/dist/index.js +175 -46
- package/dist/mailbox.d.ts +40 -3
- package/dist/mailbox.js +655 -95
- package/dist/policy.d.ts +5 -3
- package/dist/policy.js +9 -6
- package/dist/registry.d.ts +16 -0
- package/dist/registry.js +301 -45
- package/dist/relay.test.js +349 -4
- package/filesystem.ts +635 -0
- package/format.ts +14 -1
- package/index.ts +185 -40
- package/mailbox.ts +613 -91
- package/package.json +19 -1
- package/policy.ts +9 -6
- package/registry.ts +275 -42
package/dist/policy.d.ts
CHANGED
|
@@ -23,11 +23,13 @@ export declare class OutboundPolicy {
|
|
|
23
23
|
/**
|
|
24
24
|
* Gate one outbound letter. `unreadBacklog` is the target's current
|
|
25
25
|
* unread count (report 0 when the target is idle — an idle agent has by
|
|
26
|
-
* definition worked through what it was handed).
|
|
26
|
+
* definition worked through what it was handed). `target` scopes the
|
|
27
|
+
* identical-body dedupe to a single peer (loop-breaking) so a broadcast
|
|
28
|
+
* of one body to N peers is not deduped after the first.
|
|
27
29
|
*/
|
|
28
|
-
check(body: string, unreadBacklog: number): OutboundVerdict;
|
|
30
|
+
check(body: string, unreadBacklog: number, target?: string): OutboundVerdict;
|
|
29
31
|
/** Record a successful send so dedupe/rate state stays current. */
|
|
30
|
-
recordSend(body: string): void;
|
|
32
|
+
recordSend(body: string, target?: string): void;
|
|
31
33
|
}
|
|
32
34
|
/** Inbound guard: PI_RELAY_INBOUND=refuse drops all peer mail. */
|
|
33
35
|
export declare function inboundAccepts(env?: string | undefined): boolean;
|
package/dist/policy.js
CHANGED
|
@@ -20,9 +20,11 @@ export class OutboundPolicy {
|
|
|
20
20
|
/**
|
|
21
21
|
* Gate one outbound letter. `unreadBacklog` is the target's current
|
|
22
22
|
* unread count (report 0 when the target is idle — an idle agent has by
|
|
23
|
-
* definition worked through what it was handed).
|
|
23
|
+
* definition worked through what it was handed). `target` scopes the
|
|
24
|
+
* identical-body dedupe to a single peer (loop-breaking) so a broadcast
|
|
25
|
+
* of one body to N peers is not deduped after the first.
|
|
24
26
|
*/
|
|
25
|
-
check(body, unreadBacklog) {
|
|
27
|
+
check(body, unreadBacklog, target) {
|
|
26
28
|
if (body.length > MAX_BODY_CHARS) {
|
|
27
29
|
return {
|
|
28
30
|
ok: false,
|
|
@@ -36,9 +38,10 @@ export class OutboundPolicy {
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
const now = this.now();
|
|
39
|
-
const
|
|
41
|
+
const dedupeKey = `${body}\u0000${target ?? ''}`;
|
|
42
|
+
const lastSame = this.recentBodies.get(dedupeKey);
|
|
40
43
|
if (lastSame !== undefined && now - lastSame < DEDUPE_WINDOW_MS) {
|
|
41
|
-
return { ok: false, reason: 'Identical message
|
|
44
|
+
return { ok: false, reason: 'Identical message to the same peer less than 10s ago — dropped to break loops.' };
|
|
42
45
|
}
|
|
43
46
|
const windowStart = now - RATE_LIMIT_WINDOW_MS;
|
|
44
47
|
while (this.sentAt.length > 0 && this.sentAt[0] < windowStart)
|
|
@@ -52,9 +55,9 @@ export class OutboundPolicy {
|
|
|
52
55
|
return { ok: true };
|
|
53
56
|
}
|
|
54
57
|
/** Record a successful send so dedupe/rate state stays current. */
|
|
55
|
-
recordSend(body) {
|
|
58
|
+
recordSend(body, target) {
|
|
56
59
|
this.sentAt.push(this.now());
|
|
57
|
-
this.recentBodies.set(body
|
|
60
|
+
this.recentBodies.set(`${body}\u0000${target ?? ''}`, this.now());
|
|
58
61
|
// bound the dedupe map
|
|
59
62
|
if (this.recentBodies.size > 200) {
|
|
60
63
|
const cutoff = this.now() - DEDUPE_WINDOW_MS;
|
package/dist/registry.d.ts
CHANGED
|
@@ -34,6 +34,22 @@ export declare function ensureRoot(root: string): void;
|
|
|
34
34
|
export declare function recordPath(root: string, addr: string): string;
|
|
35
35
|
export declare function inboxDir(root: string, addr: string): string;
|
|
36
36
|
export declare function asksDir(root: string, addr: string): string;
|
|
37
|
+
export interface AliasRecord {
|
|
38
|
+
name: string;
|
|
39
|
+
addr: string;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
claimedAt: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function aliasesDir(root: string): string;
|
|
44
|
+
export declare function aliasPath(root: string, name: string): string;
|
|
45
|
+
export declare function writeAlias(root: string, alias: AliasRecord): void;
|
|
46
|
+
/** Alias names: lowercase alnum start, then alnum/_/-, 1-32 chars. */
|
|
47
|
+
export declare function isValidAliasName(name: string): boolean;
|
|
48
|
+
export declare function readAlias(root: string, name: string): AliasRecord | null;
|
|
49
|
+
export declare function listAliases(root: string): AliasRecord[];
|
|
50
|
+
/** Last-claim-wins: overwrites any prior owner. */
|
|
51
|
+
export declare function claimAlias(root: string, name: string, addr: string, sessionId: string): AliasRecord;
|
|
52
|
+
export declare function clearAlias(root: string, name: string): void;
|
|
37
53
|
export declare function writeRecord(root: string, record: SessionRecord): void;
|
|
38
54
|
export declare function readRecord(root: string, addr: string): SessionRecord | null;
|
|
39
55
|
/** Read-only listing, oldest first. Never mutates anything. */
|
package/dist/registry.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* - Listing has NO side effects: reading the directory never deletes.
|
|
16
16
|
*/
|
|
17
17
|
import * as crypto from 'node:crypto';
|
|
18
|
-
import * as fs from 'node:fs';
|
|
19
18
|
import * as path from 'node:path';
|
|
19
|
+
import { assertPathSegment, openRelayRoot, rethrowFilesystemError, RelayDirectoryHandle, RelayFilesystemError, } from './filesystem.js';
|
|
20
20
|
export const HEARTBEAT_STALE_MS = 45_000;
|
|
21
21
|
/** A mailbox holding undelivered mail is kept this long after last contact. */
|
|
22
22
|
export const SWEEP_MAIL_KEEP_MS = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -24,60 +24,222 @@ export function deriveAddr(cwd, sessionId) {
|
|
|
24
24
|
return crypto.createHash('sha256').update(`${cwd}${sessionId}`).digest('hex').slice(0, 12);
|
|
25
25
|
}
|
|
26
26
|
export function ensureRoot(root) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
fs.chmodSync(root, 0o700);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// best-effort on filesystems that support modes
|
|
33
|
-
}
|
|
27
|
+
const directory = openRelayRoot(root, true);
|
|
28
|
+
directory.close();
|
|
34
29
|
}
|
|
35
30
|
export function recordPath(root, addr) {
|
|
31
|
+
assertPathSegment(addr, 'relay address');
|
|
36
32
|
return path.join(root, `${addr}.json`);
|
|
37
33
|
}
|
|
38
34
|
export function inboxDir(root, addr) {
|
|
35
|
+
assertPathSegment(addr, 'relay address');
|
|
39
36
|
return path.join(root, `${addr}.inbox`);
|
|
40
37
|
}
|
|
41
38
|
export function asksDir(root, addr) {
|
|
39
|
+
assertPathSegment(addr, 'relay address');
|
|
42
40
|
return path.join(root, `${addr}.asks`);
|
|
43
41
|
}
|
|
44
|
-
export function
|
|
45
|
-
|
|
46
|
-
const file = recordPath(root, record.addr);
|
|
47
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
48
|
-
fs.writeFileSync(tmp, JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
49
|
-
fs.renameSync(tmp, file);
|
|
42
|
+
export function aliasesDir(root) {
|
|
43
|
+
return path.join(root, 'aliases');
|
|
50
44
|
}
|
|
51
|
-
export function
|
|
45
|
+
export function aliasPath(root, name) {
|
|
46
|
+
assertPathSegment(name, 'relay alias');
|
|
47
|
+
return path.join(aliasesDir(root), `${name}.json`);
|
|
48
|
+
}
|
|
49
|
+
export function writeAlias(root, alias) {
|
|
50
|
+
if (!isValidAliasName(alias.name))
|
|
51
|
+
throw new TypeError(`Invalid relay alias: ${alias.name}`);
|
|
52
|
+
const relay = openRelayRoot(root, true);
|
|
52
53
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const aliases = relay.openDirectory('aliases', true);
|
|
55
|
+
try {
|
|
56
|
+
aliases.writeFileAtomic(`${alias.name}.json`, JSON.stringify(alias, null, 2));
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
aliases.close();
|
|
56
60
|
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
relay.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Alias names: lowercase alnum start, then alnum/_/-, 1-32 chars. */
|
|
67
|
+
export function isValidAliasName(name) {
|
|
68
|
+
return /^[a-z0-9][a-z0-9_-]{0,31}$/.test(name);
|
|
69
|
+
}
|
|
70
|
+
function readAliasFromDirectory(directory, name) {
|
|
71
|
+
const raw = directory.readFile(`${name}.json`);
|
|
72
|
+
if (raw === null)
|
|
73
|
+
return null;
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (parsed && typeof parsed.name === 'string' && typeof parsed.addr === 'string')
|
|
76
|
+
return parsed;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
export function readAlias(root, name) {
|
|
80
|
+
// The name can come from LLM-controlled tool input — never let it reach
|
|
81
|
+
// the filesystem unvalidated (e.g. '@../../../../tmp/x').
|
|
82
|
+
if (!isValidAliasName(name))
|
|
83
|
+
return null;
|
|
84
|
+
const relay = openRelayRoot(root);
|
|
85
|
+
if (relay === null)
|
|
57
86
|
return null;
|
|
87
|
+
try {
|
|
88
|
+
const aliases = relay.openDirectory('aliases');
|
|
89
|
+
if (aliases === null)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
return readAliasFromDirectory(aliases, name);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
rethrowFilesystemError(error);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
aliases.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
relay.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function listAliases(root) {
|
|
107
|
+
const relay = openRelayRoot(root);
|
|
108
|
+
if (relay === null)
|
|
109
|
+
return [];
|
|
110
|
+
try {
|
|
111
|
+
const aliases = relay.openDirectory('aliases');
|
|
112
|
+
if (aliases === null)
|
|
113
|
+
return [];
|
|
114
|
+
try {
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const entry of aliases.readDirectory()) {
|
|
117
|
+
if (!entry.name.endsWith('.json'))
|
|
118
|
+
continue;
|
|
119
|
+
if (entry.isSymbolicLink())
|
|
120
|
+
throw new RelayFilesystemError(`Refusing symlinked relay alias: ${entry.name}`);
|
|
121
|
+
if (!entry.isFile())
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
const alias = readAliasFromDirectory(aliases, entry.name.slice(0, -'.json'.length));
|
|
125
|
+
if (alias)
|
|
126
|
+
out.push(alias);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
rethrowFilesystemError(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out.sort((a, b) => a.claimedAt - b.claimedAt);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
aliases.close();
|
|
136
|
+
}
|
|
58
137
|
}
|
|
59
|
-
catch {
|
|
138
|
+
catch (error) {
|
|
139
|
+
rethrowFilesystemError(error);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
relay.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Last-claim-wins: overwrites any prior owner. */
|
|
147
|
+
export function claimAlias(root, name, addr, sessionId) {
|
|
148
|
+
const alias = { name, addr, sessionId, claimedAt: Date.now() };
|
|
149
|
+
writeAlias(root, alias);
|
|
150
|
+
return alias;
|
|
151
|
+
}
|
|
152
|
+
export function clearAlias(root, name) {
|
|
153
|
+
if (!isValidAliasName(name))
|
|
154
|
+
return;
|
|
155
|
+
const relay = openRelayRoot(root);
|
|
156
|
+
if (relay === null)
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
const aliases = relay.openDirectory('aliases');
|
|
160
|
+
if (aliases === null)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
aliases.unlinkFile(`${name}.json`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
rethrowFilesystemError(error);
|
|
167
|
+
// deletion remains best-effort for ordinary I/O failures
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
aliases.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
relay.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function writeRecord(root, record) {
|
|
178
|
+
const relay = openRelayRoot(root, true);
|
|
179
|
+
try {
|
|
180
|
+
relay.writeFileAtomic(`${record.addr}.json`, JSON.stringify(record, null, 2));
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
relay.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function readRecordFromDirectory(directory, addr) {
|
|
187
|
+
const raw = directory.readFile(`${addr}.json`);
|
|
188
|
+
if (raw === null)
|
|
60
189
|
return null;
|
|
190
|
+
const parsed = JSON.parse(raw);
|
|
191
|
+
if (parsed && typeof parsed.addr === 'string' && typeof parsed.pid === 'number')
|
|
192
|
+
return parsed;
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
export function readRecord(root, addr) {
|
|
196
|
+
assertPathSegment(addr, 'relay address');
|
|
197
|
+
const relay = openRelayRoot(root);
|
|
198
|
+
if (relay === null)
|
|
199
|
+
return null;
|
|
200
|
+
try {
|
|
201
|
+
return readRecordFromDirectory(relay, addr);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
rethrowFilesystemError(error);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
relay.close();
|
|
61
209
|
}
|
|
62
210
|
}
|
|
63
211
|
/** Read-only listing, oldest first. Never mutates anything. */
|
|
64
212
|
export function listRecords(root) {
|
|
65
|
-
|
|
213
|
+
const relay = openRelayRoot(root);
|
|
214
|
+
if (relay === null)
|
|
215
|
+
return [];
|
|
66
216
|
try {
|
|
67
|
-
|
|
217
|
+
const out = [];
|
|
218
|
+
for (const entry of relay.readDirectory()) {
|
|
219
|
+
if (!entry.name.endsWith('.json'))
|
|
220
|
+
continue;
|
|
221
|
+
if (entry.isSymbolicLink())
|
|
222
|
+
throw new RelayFilesystemError(`Refusing symlinked relay record: ${entry.name}`);
|
|
223
|
+
if (!entry.isFile())
|
|
224
|
+
continue;
|
|
225
|
+
try {
|
|
226
|
+
const record = readRecordFromDirectory(relay, entry.name.slice(0, -'.json'.length));
|
|
227
|
+
if (record)
|
|
228
|
+
out.push(record);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
rethrowFilesystemError(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out.sort((a, b) => a.startedAt - b.startedAt);
|
|
68
235
|
}
|
|
69
|
-
catch {
|
|
236
|
+
catch (error) {
|
|
237
|
+
rethrowFilesystemError(error);
|
|
70
238
|
return [];
|
|
71
239
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
75
|
-
continue;
|
|
76
|
-
const record = readRecord(root, entry.name.slice(0, -'.json'.length));
|
|
77
|
-
if (record)
|
|
78
|
-
out.push(record);
|
|
240
|
+
finally {
|
|
241
|
+
relay.close();
|
|
79
242
|
}
|
|
80
|
-
return out.sort((a, b) => a.startedAt - b.startedAt);
|
|
81
243
|
}
|
|
82
244
|
function pidAlive(pid) {
|
|
83
245
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -98,21 +260,96 @@ export function presenceOf(record, now = Date.now()) {
|
|
|
98
260
|
return 'offline';
|
|
99
261
|
return now - record.lastSeenAt < HEARTBEAT_STALE_MS ? 'live' : 'stalled';
|
|
100
262
|
}
|
|
101
|
-
function dirHasJson(
|
|
263
|
+
function dirHasJson(root, name) {
|
|
264
|
+
const relay = openRelayRoot(root);
|
|
265
|
+
if (relay === null)
|
|
266
|
+
return false;
|
|
102
267
|
try {
|
|
103
|
-
|
|
268
|
+
const directory = relay.openDirectory(name);
|
|
269
|
+
if (directory === null)
|
|
270
|
+
return false;
|
|
271
|
+
try {
|
|
272
|
+
return directory.readDirectory().some((entry) => {
|
|
273
|
+
if (!entry.name.endsWith('.json'))
|
|
274
|
+
return false;
|
|
275
|
+
if (entry.isSymbolicLink())
|
|
276
|
+
throw new RelayFilesystemError(`Refusing symlinked relay entry: ${entry.name}`);
|
|
277
|
+
return entry.isFile();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
directory.close();
|
|
282
|
+
}
|
|
104
283
|
}
|
|
105
|
-
catch {
|
|
284
|
+
catch (error) {
|
|
285
|
+
rethrowFilesystemError(error);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
relay.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function claimsHaveJson(root, addr) {
|
|
293
|
+
const relay = openRelayRoot(root);
|
|
294
|
+
if (relay === null)
|
|
106
295
|
return false;
|
|
296
|
+
try {
|
|
297
|
+
const claims = relay.openDirectory(`${addr}.claims`);
|
|
298
|
+
if (claims === null)
|
|
299
|
+
return false;
|
|
300
|
+
try {
|
|
301
|
+
for (const entry of claims.readDirectory()) {
|
|
302
|
+
if (entry.isSymbolicLink())
|
|
303
|
+
throw new RelayFilesystemError(`Refusing symlinked relay claim: ${entry.name}`);
|
|
304
|
+
if (!entry.isDirectory())
|
|
305
|
+
continue;
|
|
306
|
+
const claim = claims.openDirectory(entry.name);
|
|
307
|
+
if (claim === null)
|
|
308
|
+
continue;
|
|
309
|
+
try {
|
|
310
|
+
if (claim.readDirectory().some((file) => {
|
|
311
|
+
if (!file.name.endsWith('.json'))
|
|
312
|
+
return false;
|
|
313
|
+
if (file.isSymbolicLink()) {
|
|
314
|
+
throw new RelayFilesystemError(`Refusing symlinked claimed letter: ${file.name}`);
|
|
315
|
+
}
|
|
316
|
+
return file.isFile();
|
|
317
|
+
})) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
claim.close();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
claims.close();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
relay.close();
|
|
107
333
|
}
|
|
108
334
|
}
|
|
109
|
-
function
|
|
335
|
+
function removeClaims(directory, addr) {
|
|
336
|
+
const claimsName = `${addr}.claims`;
|
|
337
|
+
const claims = directory.openDirectory(claimsName);
|
|
338
|
+
if (claims === null)
|
|
339
|
+
return;
|
|
110
340
|
try {
|
|
111
|
-
|
|
341
|
+
for (const entry of claims.readDirectory()) {
|
|
342
|
+
if (entry.isSymbolicLink())
|
|
343
|
+
throw new RelayFilesystemError(`Refusing symlinked relay claim: ${entry.name}`);
|
|
344
|
+
if (!entry.isDirectory())
|
|
345
|
+
continue;
|
|
346
|
+
claims.removeDirectory(entry.name);
|
|
347
|
+
}
|
|
112
348
|
}
|
|
113
|
-
|
|
114
|
-
|
|
349
|
+
finally {
|
|
350
|
+
claims.close();
|
|
115
351
|
}
|
|
352
|
+
directory.removeDirectory(claimsName);
|
|
116
353
|
}
|
|
117
354
|
/**
|
|
118
355
|
* Reclaim dead sessions' files. Rules (mail outranks tidiness):
|
|
@@ -131,19 +368,38 @@ export function sweep(root, now = Date.now(), sessionExists) {
|
|
|
131
368
|
for (const record of listRecords(root)) {
|
|
132
369
|
if (presenceOf(record, now) !== 'offline')
|
|
133
370
|
continue;
|
|
134
|
-
const hasMail = dirHasJson(
|
|
371
|
+
const hasMail = dirHasJson(root, `${record.addr}.inbox`) ||
|
|
372
|
+
dirHasJson(root, `${record.addr}.asks`) ||
|
|
373
|
+
claimsHaveJson(root, record.addr);
|
|
135
374
|
const expired = now - record.lastSeenAt >= SWEEP_MAIL_KEEP_MS;
|
|
136
375
|
if (hasMail && !expired)
|
|
137
376
|
continue;
|
|
138
377
|
if (!expired && (sessionExists?.(record.sessionId) ?? true))
|
|
139
378
|
continue;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
379
|
+
const relay = openRelayRoot(root);
|
|
380
|
+
if (relay !== null) {
|
|
381
|
+
try {
|
|
382
|
+
relay.removeDirectory(`${record.addr}.inbox`);
|
|
383
|
+
relay.removeDirectory(`${record.addr}.asks`);
|
|
384
|
+
removeClaims(relay, record.addr);
|
|
385
|
+
try {
|
|
386
|
+
relay.unlinkFile(`${record.addr}.json`);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
rethrowFilesystemError(error);
|
|
390
|
+
// deletion remains best-effort for ordinary I/O failures
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
relay.close();
|
|
395
|
+
}
|
|
147
396
|
}
|
|
148
397
|
}
|
|
398
|
+
// Aliases are swept when the owning session dies: an alias whose record
|
|
399
|
+
// was just reaped (or never existed) is reclaimed. A resumable-but-offline
|
|
400
|
+
// session keeps its record, so it keeps its alias too.
|
|
401
|
+
for (const alias of listAliases(root)) {
|
|
402
|
+
if (readRecord(root, alias.addr) === null)
|
|
403
|
+
clearAlias(root, alias.name);
|
|
404
|
+
}
|
|
149
405
|
}
|