@meshwhisper/sdk 0.1.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 +138 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +19 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chaff/index.d.ts +91 -0
- package/dist/chaff/index.d.ts.map +1 -0
- package/dist/chaff/index.js +268 -0
- package/dist/chaff/index.js.map +1 -0
- package/dist/cluster/index.d.ts +159 -0
- package/dist/cluster/index.d.ts.map +1 -0
- package/dist/cluster/index.js +393 -0
- package/dist/cluster/index.js.map +1 -0
- package/dist/compliance/index.d.ts +129 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +315 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/crypto/index.d.ts +65 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +146 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/group/index.d.ts +155 -0
- package/dist/group/index.d.ts.map +1 -0
- package/dist/group/index.js +560 -0
- package/dist/group/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/namespace/index.d.ts +155 -0
- package/dist/namespace/index.d.ts.map +1 -0
- package/dist/namespace/index.js +278 -0
- package/dist/namespace/index.js.map +1 -0
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +19 -0
- package/dist/node/index.js.map +1 -0
- package/dist/packet/index.d.ts +63 -0
- package/dist/packet/index.d.ts.map +1 -0
- package/dist/packet/index.js +244 -0
- package/dist/packet/index.js.map +1 -0
- package/dist/permissions/index.d.ts +107 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +282 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/persistence/idb-storage.d.ts +27 -0
- package/dist/persistence/idb-storage.d.ts.map +1 -0
- package/dist/persistence/idb-storage.js +75 -0
- package/dist/persistence/idb-storage.js.map +1 -0
- package/dist/persistence/index.d.ts +4 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/node-storage.d.ts +33 -0
- package/dist/persistence/node-storage.d.ts.map +1 -0
- package/dist/persistence/node-storage.js +90 -0
- package/dist/persistence/node-storage.js.map +1 -0
- package/dist/persistence/serialization.d.ts +4 -0
- package/dist/persistence/serialization.d.ts.map +1 -0
- package/dist/persistence/serialization.js +49 -0
- package/dist/persistence/serialization.js.map +1 -0
- package/dist/persistence/types.d.ts +29 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +5 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/ratchet/index.d.ts +80 -0
- package/dist/ratchet/index.d.ts.map +1 -0
- package/dist/ratchet/index.js +259 -0
- package/dist/ratchet/index.js.map +1 -0
- package/dist/reciprocity/index.d.ts +109 -0
- package/dist/reciprocity/index.d.ts.map +1 -0
- package/dist/reciprocity/index.js +311 -0
- package/dist/reciprocity/index.js.map +1 -0
- package/dist/relay/index.d.ts +87 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/index.js +286 -0
- package/dist/relay/index.js.map +1 -0
- package/dist/routing/index.d.ts +136 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +478 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sdk/index.d.ts +322 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +1530 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sybil/index.d.ts +123 -0
- package/dist/sybil/index.d.ts.map +1 -0
- package/dist/sybil/index.js +491 -0
- package/dist/sybil/index.js.map +1 -0
- package/dist/transport/browser/index.d.ts +34 -0
- package/dist/transport/browser/index.d.ts.map +1 -0
- package/dist/transport/browser/index.js +176 -0
- package/dist/transport/browser/index.js.map +1 -0
- package/dist/transport/local/index.d.ts +57 -0
- package/dist/transport/local/index.d.ts.map +1 -0
- package/dist/transport/local/index.js +442 -0
- package/dist/transport/local/index.js.map +1 -0
- package/dist/transport/negotiator/index.d.ts +79 -0
- package/dist/transport/negotiator/index.d.ts.map +1 -0
- package/dist/transport/negotiator/index.js +289 -0
- package/dist/transport/negotiator/index.js.map +1 -0
- package/dist/transport/node/index.d.ts +56 -0
- package/dist/transport/node/index.d.ts.map +1 -0
- package/dist/transport/node/index.js +209 -0
- package/dist/transport/node/index.js.map +1 -0
- package/dist/transport/noop/index.d.ts +11 -0
- package/dist/transport/noop/index.d.ts.map +1 -0
- package/dist/transport/noop/index.js +20 -0
- package/dist/transport/noop/index.js.map +1 -0
- package/dist/transport/p2p/index.d.ts +109 -0
- package/dist/transport/p2p/index.d.ts.map +1 -0
- package/dist/transport/p2p/index.js +237 -0
- package/dist/transport/p2p/index.js.map +1 -0
- package/dist/transport/websocket/index.d.ts +89 -0
- package/dist/transport/websocket/index.d.ts.map +1 -0
- package/dist/transport/websocket/index.js +498 -0
- package/dist/transport/websocket/index.js.map +1 -0
- package/dist/transport/websocket/serialize.d.ts +5 -0
- package/dist/transport/websocket/serialize.d.ts.map +1 -0
- package/dist/transport/websocket/serialize.js +55 -0
- package/dist/transport/websocket/serialize.js.map +1 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/x3dh/index.d.ts +120 -0
- package/dist/x3dh/index.d.ts.map +1 -0
- package/dist/x3dh/index.js +290 -0
- package/dist/x3dh/index.js.map +1 -0
- package/package.json +59 -0
- package/src/browser/index.ts +19 -0
- package/src/chaff/index.ts +340 -0
- package/src/cluster/index.ts +482 -0
- package/src/compliance/index.ts +407 -0
- package/src/crypto/index.ts +193 -0
- package/src/group/index.ts +719 -0
- package/src/index.ts +87 -0
- package/src/lz4js.d.ts +58 -0
- package/src/namespace/index.ts +336 -0
- package/src/node/index.ts +19 -0
- package/src/packet/index.ts +326 -0
- package/src/permissions/index.ts +405 -0
- package/src/persistence/idb-storage.ts +83 -0
- package/src/persistence/index.ts +3 -0
- package/src/persistence/node-storage.ts +96 -0
- package/src/persistence/serialization.ts +75 -0
- package/src/persistence/types.ts +33 -0
- package/src/ratchet/index.ts +363 -0
- package/src/reciprocity/index.ts +371 -0
- package/src/relay/index.ts +382 -0
- package/src/routing/index.ts +577 -0
- package/src/sdk/index.ts +1994 -0
- package/src/sybil/index.ts +661 -0
- package/src/transport/browser/index.ts +201 -0
- package/src/transport/local/index.ts +540 -0
- package/src/transport/negotiator/index.ts +397 -0
- package/src/transport/node/index.ts +234 -0
- package/src/transport/noop/index.ts +22 -0
- package/src/transport/p2p/index.ts +345 -0
- package/src/transport/websocket/index.ts +660 -0
- package/src/transport/websocket/serialize.ts +68 -0
- package/src/types.ts +275 -0
- package/src/x3dh/index.ts +388 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — Store-and-Forward Relay Module
|
|
3
|
+
// Stores encrypted blobs for offline peers and delivers them
|
|
4
|
+
// when the recipient announces presence.
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import type { StoredBlob } from "../types";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Generate a random 32-hex-char blob ID. */
|
|
14
|
+
function generateBlobId(): string {
|
|
15
|
+
// crypto.randomUUID() is available in Node >= 19 and modern runtimes,
|
|
16
|
+
// but a hex string is more portable and avoids dashes.
|
|
17
|
+
const bytes = new Uint8Array(16);
|
|
18
|
+
crypto.getRandomValues(bytes);
|
|
19
|
+
return Array.from(bytes)
|
|
20
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
21
|
+
.join("");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Constant-time-ish comparison for Uint8Array equality. */
|
|
25
|
+
function uint8Equal(a: Uint8Array, b: Uint8Array): boolean {
|
|
26
|
+
if (a.length !== b.length) return false;
|
|
27
|
+
let diff = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
diff |= a[i] ^ b[i];
|
|
30
|
+
}
|
|
31
|
+
return diff === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Rough byte size of a StoredBlob (payload dominates, but we account for metadata too). */
|
|
35
|
+
function blobByteSize(blob: StoredBlob): number {
|
|
36
|
+
return (
|
|
37
|
+
blob.encryptedPayload.byteLength +
|
|
38
|
+
blob.destHash.byteLength +
|
|
39
|
+
blob.id.length * 2 + // JS string ≈ 2 bytes per char
|
|
40
|
+
32 // fixed overhead for numeric fields + object shell
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Default constants
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const DEFAULT_MAX_STORAGE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
49
|
+
const DEFAULT_TTL_HOURS = 72;
|
|
50
|
+
const DEFAULT_PRUNE_INTERVAL_MS = 60_000; // 1 minute
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// RelayStore
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export interface RelayStoreOptions {
|
|
57
|
+
maxStorageBytes?: number;
|
|
58
|
+
defaultTTLHours?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class RelayStore {
|
|
62
|
+
private readonly maxStorageBytes: number;
|
|
63
|
+
private readonly defaultTTLHours: number;
|
|
64
|
+
|
|
65
|
+
/** All stored blobs keyed by blob id. */
|
|
66
|
+
private readonly blobs = new Map<string, StoredBlob>();
|
|
67
|
+
|
|
68
|
+
/** Secondary index: destHash hex -> set of blob ids. */
|
|
69
|
+
private readonly destIndex = new Map<string, Set<string>>();
|
|
70
|
+
|
|
71
|
+
/** Track insertion order for LRU eviction (oldest first). */
|
|
72
|
+
private readonly insertionOrder: string[] = [];
|
|
73
|
+
|
|
74
|
+
private currentStorageBytes = 0;
|
|
75
|
+
private pruneTimer: ReturnType<typeof setInterval> | null = null;
|
|
76
|
+
|
|
77
|
+
constructor(options?: RelayStoreOptions) {
|
|
78
|
+
this.maxStorageBytes = options?.maxStorageBytes ?? DEFAULT_MAX_STORAGE_BYTES;
|
|
79
|
+
this.defaultTTLHours = options?.defaultTTLHours ?? DEFAULT_TTL_HOURS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ----- helpers -----
|
|
83
|
+
|
|
84
|
+
private destKey(destHash: Uint8Array): string {
|
|
85
|
+
return Array.from(destHash)
|
|
86
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
87
|
+
.join("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private isExpired(blob: StoredBlob): boolean {
|
|
91
|
+
const expiryMs = blob.receivedAt + blob.ttlHours * 3_600_000;
|
|
92
|
+
return Date.now() >= expiryMs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ----- Blob storage -----
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Store an encrypted blob. Returns false if storage is full even after
|
|
99
|
+
* evicting the oldest blobs.
|
|
100
|
+
*/
|
|
101
|
+
storeBlob(blob: StoredBlob): boolean {
|
|
102
|
+
const size = blobByteSize(blob);
|
|
103
|
+
|
|
104
|
+
// Evict oldest blobs until there is room (or nothing left to evict)
|
|
105
|
+
while (
|
|
106
|
+
this.currentStorageBytes + size > this.maxStorageBytes &&
|
|
107
|
+
this.insertionOrder.length > 0
|
|
108
|
+
) {
|
|
109
|
+
const oldestId = this.insertionOrder[0];
|
|
110
|
+
this.removeBlob(oldestId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this.currentStorageBytes + size > this.maxStorageBytes) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Avoid duplicate ids
|
|
118
|
+
if (this.blobs.has(blob.id)) {
|
|
119
|
+
this.removeBlob(blob.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.blobs.set(blob.id, blob);
|
|
123
|
+
this.currentStorageBytes += size;
|
|
124
|
+
this.insertionOrder.push(blob.id);
|
|
125
|
+
|
|
126
|
+
const dk = this.destKey(blob.destHash);
|
|
127
|
+
let set = this.destIndex.get(dk);
|
|
128
|
+
if (!set) {
|
|
129
|
+
set = new Set();
|
|
130
|
+
this.destIndex.set(dk, set);
|
|
131
|
+
}
|
|
132
|
+
set.add(blob.id);
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Retrieve all stored blobs for a given destination hash. */
|
|
138
|
+
getBlobs(destHash: Uint8Array): StoredBlob[] {
|
|
139
|
+
const dk = this.destKey(destHash);
|
|
140
|
+
const ids = this.destIndex.get(dk);
|
|
141
|
+
if (!ids) return [];
|
|
142
|
+
const results: StoredBlob[] = [];
|
|
143
|
+
for (const id of ids) {
|
|
144
|
+
const blob = this.blobs.get(id);
|
|
145
|
+
if (blob) results.push(blob);
|
|
146
|
+
}
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Remove a single blob by id. */
|
|
151
|
+
removeBlob(id: string): void {
|
|
152
|
+
const blob = this.blobs.get(id);
|
|
153
|
+
if (!blob) return;
|
|
154
|
+
|
|
155
|
+
this.currentStorageBytes -= blobByteSize(blob);
|
|
156
|
+
this.blobs.delete(id);
|
|
157
|
+
|
|
158
|
+
const dk = this.destKey(blob.destHash);
|
|
159
|
+
const set = this.destIndex.get(dk);
|
|
160
|
+
if (set) {
|
|
161
|
+
set.delete(id);
|
|
162
|
+
if (set.size === 0) this.destIndex.delete(dk);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const idx = this.insertionOrder.indexOf(id);
|
|
166
|
+
if (idx !== -1) this.insertionOrder.splice(idx, 1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Remove all blobs targeting a specific destination hash. */
|
|
170
|
+
removeBlobsForDest(destHash: Uint8Array): void {
|
|
171
|
+
const dk = this.destKey(destHash);
|
|
172
|
+
const ids = this.destIndex.get(dk);
|
|
173
|
+
if (!ids) return;
|
|
174
|
+
// Snapshot the ids before iterating — removeBlob mutates the set.
|
|
175
|
+
for (const id of [...ids]) {
|
|
176
|
+
this.removeBlob(id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ----- TTL management -----
|
|
181
|
+
|
|
182
|
+
/** Remove all expired blobs. Returns count of blobs removed. */
|
|
183
|
+
pruneExpired(): number {
|
|
184
|
+
let removed = 0;
|
|
185
|
+
// Iterate oldest-first for efficiency.
|
|
186
|
+
for (let i = this.insertionOrder.length - 1; i >= 0; i--) {
|
|
187
|
+
const id = this.insertionOrder[i];
|
|
188
|
+
const blob = this.blobs.get(id);
|
|
189
|
+
if (blob && this.isExpired(blob)) {
|
|
190
|
+
this.removeBlob(id);
|
|
191
|
+
removed++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return removed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Start automatic periodic pruning. */
|
|
198
|
+
startPruneInterval(intervalMs?: number): void {
|
|
199
|
+
this.stopPruneInterval();
|
|
200
|
+
this.pruneTimer = setInterval(
|
|
201
|
+
() => this.pruneExpired(),
|
|
202
|
+
intervalMs ?? DEFAULT_PRUNE_INTERVAL_MS,
|
|
203
|
+
);
|
|
204
|
+
// Allow the Node.js process to exit even if the timer is running.
|
|
205
|
+
if (typeof this.pruneTimer === "object" && "unref" in this.pruneTimer) {
|
|
206
|
+
this.pruneTimer.unref();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Stop automatic periodic pruning. */
|
|
211
|
+
stopPruneInterval(): void {
|
|
212
|
+
if (this.pruneTimer !== null) {
|
|
213
|
+
clearInterval(this.pruneTimer);
|
|
214
|
+
this.pruneTimer = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ----- Storage metrics -----
|
|
219
|
+
|
|
220
|
+
/** Bytes currently consumed by stored blobs. */
|
|
221
|
+
getStorageUsed(): number {
|
|
222
|
+
return this.currentStorageBytes;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Configured maximum storage capacity in bytes. */
|
|
226
|
+
getStorageCapacity(): number {
|
|
227
|
+
return this.maxStorageBytes;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Total number of blobs currently stored. */
|
|
231
|
+
getBlobCount(): number {
|
|
232
|
+
return this.blobs.size;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ----- Presence announcement -----
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Handle a presence announcement. When a peer announces it is online we
|
|
239
|
+
* return (and remove) all blobs matching its destination hash.
|
|
240
|
+
*
|
|
241
|
+
* Destination hashes rotate hourly. The caller should provide both the
|
|
242
|
+
* current-epoch and the previous-epoch destination hashes so we can match
|
|
243
|
+
* blobs stored under either.
|
|
244
|
+
*/
|
|
245
|
+
handlePresenceAnnouncement(destHash: Uint8Array): StoredBlob[];
|
|
246
|
+
handlePresenceAnnouncement(
|
|
247
|
+
destHash: Uint8Array,
|
|
248
|
+
previousEpochDestHash?: Uint8Array,
|
|
249
|
+
): StoredBlob[];
|
|
250
|
+
handlePresenceAnnouncement(
|
|
251
|
+
destHash: Uint8Array,
|
|
252
|
+
previousEpochDestHash?: Uint8Array,
|
|
253
|
+
): StoredBlob[] {
|
|
254
|
+
const delivered: StoredBlob[] = [];
|
|
255
|
+
|
|
256
|
+
// Current epoch
|
|
257
|
+
const currentBlobs = this.getBlobs(destHash);
|
|
258
|
+
delivered.push(...currentBlobs);
|
|
259
|
+
this.removeBlobsForDest(destHash);
|
|
260
|
+
|
|
261
|
+
// Previous epoch (dest hashes rotate hourly)
|
|
262
|
+
if (previousEpochDestHash && !uint8Equal(destHash, previousEpochDestHash)) {
|
|
263
|
+
const prevBlobs = this.getBlobs(previousEpochDestHash);
|
|
264
|
+
delivered.push(...prevBlobs);
|
|
265
|
+
this.removeBlobsForDest(previousEpochDestHash);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return delivered;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ----- Multi-hop forwarding -----
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Decide whether a blob should be forwarded to a closer relay.
|
|
275
|
+
* Returns true when the blob still has remaining hops and a closer relay
|
|
276
|
+
* is available.
|
|
277
|
+
*/
|
|
278
|
+
shouldForward(blob: StoredBlob, closerRelayAvailable: boolean): boolean {
|
|
279
|
+
return closerRelayAvailable && blob.hopsRemaining > 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Prepare a blob for multi-hop forwarding.
|
|
284
|
+
* Returns a new StoredBlob with decremented hopsRemaining and a fresh id.
|
|
285
|
+
*/
|
|
286
|
+
prepareForward(blob: StoredBlob): StoredBlob {
|
|
287
|
+
if (blob.hopsRemaining <= 0) {
|
|
288
|
+
throw new Error("Cannot forward blob with no remaining hops");
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
...blob,
|
|
292
|
+
id: generateBlobId(),
|
|
293
|
+
hopsRemaining: blob.hopsRemaining - 1,
|
|
294
|
+
receivedAt: Date.now(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// StoreAndForwardManager — higher-level orchestration
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
export interface StoreAndForwardStats {
|
|
304
|
+
blobCount: number;
|
|
305
|
+
bytesUsed: number;
|
|
306
|
+
oldestBlobAge: number; // milliseconds since the oldest blob was received
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export class StoreAndForwardManager {
|
|
310
|
+
private readonly store: RelayStore;
|
|
311
|
+
|
|
312
|
+
constructor(store: RelayStore) {
|
|
313
|
+
this.store = store;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Store an encrypted payload destined for an offline peer.
|
|
318
|
+
* Returns the generated blob id, or throws if storage is completely full.
|
|
319
|
+
*/
|
|
320
|
+
storeForDelivery(
|
|
321
|
+
destHash: Uint8Array,
|
|
322
|
+
encryptedPayload: Uint8Array,
|
|
323
|
+
ttlHours?: number,
|
|
324
|
+
): string {
|
|
325
|
+
const blob: StoredBlob = {
|
|
326
|
+
id: generateBlobId(),
|
|
327
|
+
destHash,
|
|
328
|
+
encryptedPayload,
|
|
329
|
+
receivedAt: Date.now(),
|
|
330
|
+
ttlHours: ttlHours ?? 72,
|
|
331
|
+
hopsRemaining: 3, // sensible default for multi-hop
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const stored = this.store.storeBlob(blob);
|
|
335
|
+
if (!stored) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
"Relay storage full: could not store blob even after eviction",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return blob.id;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Called when a peer comes online. Returns all stored blobs for its
|
|
346
|
+
* destination hash and removes them from the store.
|
|
347
|
+
*/
|
|
348
|
+
deliverStored(destHash: Uint8Array): StoredBlob[] {
|
|
349
|
+
return this.store.handlePresenceAnnouncement(destHash);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Return aggregate stats about the relay store. */
|
|
353
|
+
getStats(): StoreAndForwardStats {
|
|
354
|
+
const blobCount = this.store.getBlobCount();
|
|
355
|
+
const bytesUsed = this.store.getStorageUsed();
|
|
356
|
+
|
|
357
|
+
let oldestBlobAge = 0;
|
|
358
|
+
if (blobCount > 0) {
|
|
359
|
+
// We don't have direct access to the internal insertion order,
|
|
360
|
+
// so scan all blobs via a destHash-agnostic approach: use getBlobs
|
|
361
|
+
// is not feasible without knowing hashes. Instead we expose a
|
|
362
|
+
// helper below or compute from the public API. Since we control
|
|
363
|
+
// both classes in this module, we reach into the store's blobs map
|
|
364
|
+
// via a dedicated accessor.
|
|
365
|
+
oldestBlobAge = this.computeOldestAge();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { blobCount, bytesUsed, oldestBlobAge };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private computeOldestAge(): number {
|
|
372
|
+
// Access internal state via the store — both classes live in the same
|
|
373
|
+
// module, so this tightly-coupled reach is acceptable.
|
|
374
|
+
const blobs = (this.store as unknown as { blobs: Map<string, StoredBlob> })
|
|
375
|
+
.blobs;
|
|
376
|
+
let oldest = Infinity;
|
|
377
|
+
for (const blob of blobs.values()) {
|
|
378
|
+
if (blob.receivedAt < oldest) oldest = blob.receivedAt;
|
|
379
|
+
}
|
|
380
|
+
return oldest === Infinity ? 0 : Date.now() - oldest;
|
|
381
|
+
}
|
|
382
|
+
}
|