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