@decentnetwork/peer 0.1.150 → 0.1.152
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/dist/peer.d.ts +19 -1
- package/dist/peer.js +79 -2
- package/package.json +1 -1
package/dist/peer.d.ts
CHANGED
|
@@ -80,13 +80,31 @@ export declare class Peer {
|
|
|
80
80
|
};
|
|
81
81
|
addKnownNodes(nodes: NetworkNode[]): void;
|
|
82
82
|
knownNodes(): NetworkNode[];
|
|
83
|
-
|
|
83
|
+
/** @param opts.force Send even when this friend looks already-accepted.
|
|
84
|
+
*
|
|
85
|
+
* The idempotency guard below is right for automatic retries and wrong for
|
|
86
|
+
* a person clicking "add". If the other side REJECTED an earlier request,
|
|
87
|
+
* their app forgets it — but a net_crypto session may already have been
|
|
88
|
+
* established, so this side keeps acceptedAt and skips every later attempt.
|
|
89
|
+
* Neither peer is then in the other's friend list and nothing either user
|
|
90
|
+
* does can fix it: every add is silently dropped here. A deliberate add has
|
|
91
|
+
* to be able to say "I mean it". */
|
|
92
|
+
sendFriendRequest(pubkey: string, hello?: string, opts?: {
|
|
93
|
+
force?: boolean;
|
|
94
|
+
}): Promise<void>;
|
|
84
95
|
lastFriendRequestDispatch(): {
|
|
85
96
|
transport: "onion" | "direct";
|
|
86
97
|
routes: number;
|
|
87
98
|
targets: number;
|
|
88
99
|
} | undefined;
|
|
89
100
|
acceptFriendRequest(pubkey: string): Promise<void>;
|
|
101
|
+
/** Decline an inbound request.
|
|
102
|
+
*
|
|
103
|
+
* Also drops any friend record and session the request already created.
|
|
104
|
+
* Clearing only the pending list left the transport half-attached: the
|
|
105
|
+
* sender saw a session, marked us accepted, and its idempotency guard then
|
|
106
|
+
* skipped every later attempt — so a rejected peer could never ask again,
|
|
107
|
+
* and neither side could see why. */
|
|
90
108
|
rejectFriendRequest(pubkey: string): void;
|
|
91
109
|
/**
|
|
92
110
|
* Drop a friend entirely: tear down any active net_crypto session, forget
|
package/dist/peer.js
CHANGED
|
@@ -395,6 +395,7 @@ export class Peer {
|
|
|
395
395
|
#textHandlers = new Set();
|
|
396
396
|
#pendingTextAcks = new Map();
|
|
397
397
|
#deliveredTextIds = new Set();
|
|
398
|
+
#deliveredPersistTimer;
|
|
398
399
|
#deliveredTextOrder = [];
|
|
399
400
|
#friendStoreFile;
|
|
400
401
|
#persistSeq = 0; // makes atomic friend-store temp filenames unique per write
|
|
@@ -598,6 +599,7 @@ export class Peer {
|
|
|
598
599
|
}
|
|
599
600
|
this.#cookieSymmetricKey = randomBytes(32);
|
|
600
601
|
await this.#loadPersistedFriends();
|
|
602
|
+
await this.#loadDeliveredTextIds();
|
|
601
603
|
if (this.#opts.expressNodes && this.#opts.expressNodes.length > 0) {
|
|
602
604
|
this.#express = new LegacyExpressClient({
|
|
603
605
|
nodes: this.#opts.expressNodes,
|
|
@@ -1038,7 +1040,16 @@ export class Peer {
|
|
|
1038
1040
|
knownNodes() {
|
|
1039
1041
|
return [...this.#knownNodes];
|
|
1040
1042
|
}
|
|
1041
|
-
|
|
1043
|
+
/** @param opts.force Send even when this friend looks already-accepted.
|
|
1044
|
+
*
|
|
1045
|
+
* The idempotency guard below is right for automatic retries and wrong for
|
|
1046
|
+
* a person clicking "add". If the other side REJECTED an earlier request,
|
|
1047
|
+
* their app forgets it — but a net_crypto session may already have been
|
|
1048
|
+
* established, so this side keeps acceptedAt and skips every later attempt.
|
|
1049
|
+
* Neither peer is then in the other's friend list and nothing either user
|
|
1050
|
+
* does can fix it: every add is silently dropped here. A deliberate add has
|
|
1051
|
+
* to be able to say "I mean it". */
|
|
1052
|
+
async sendFriendRequest(pubkey, hello, opts) {
|
|
1042
1053
|
if (!this.#keyPair || !this.#announceDataKey) {
|
|
1043
1054
|
throw new Error("Peer is not started");
|
|
1044
1055
|
}
|
|
@@ -1054,7 +1065,7 @@ export class Peer {
|
|
|
1054
1065
|
const friendAddress = parseCarrierAddress(pubkey);
|
|
1055
1066
|
const friendId = carrierIdFromPublicKey(friendAddress.publicKey);
|
|
1056
1067
|
const existing = this.#friends.get(friendId);
|
|
1057
|
-
if (existing?.acceptedAt) {
|
|
1068
|
+
if (existing?.acceptedAt && !opts?.force) {
|
|
1058
1069
|
this.#debugLog(`friend request skipped — ${friendId} already accepted at ${new Date(existing.acceptedAt).toISOString()}`);
|
|
1059
1070
|
this.#lastFriendRequestDispatch = {
|
|
1060
1071
|
transport: "onion",
|
|
@@ -1235,8 +1246,17 @@ export class Peer {
|
|
|
1235
1246
|
this.#debugLog(`accept friend: initiate session failed for ${pubkey}: ${error.message}`);
|
|
1236
1247
|
});
|
|
1237
1248
|
}
|
|
1249
|
+
/** Decline an inbound request.
|
|
1250
|
+
*
|
|
1251
|
+
* Also drops any friend record and session the request already created.
|
|
1252
|
+
* Clearing only the pending list left the transport half-attached: the
|
|
1253
|
+
* sender saw a session, marked us accepted, and its idempotency guard then
|
|
1254
|
+
* skipped every later attempt — so a rejected peer could never ask again,
|
|
1255
|
+
* and neither side could see why. */
|
|
1238
1256
|
rejectFriendRequest(pubkey) {
|
|
1239
1257
|
this.#pendingFriendRequests.delete(pubkey);
|
|
1258
|
+
if (this.#friends.has(pubkey))
|
|
1259
|
+
this.removeFriend(pubkey);
|
|
1240
1260
|
}
|
|
1241
1261
|
/**
|
|
1242
1262
|
* Drop a friend entirely: tear down any active net_crypto session, forget
|
|
@@ -1623,6 +1643,63 @@ export class Peer {
|
|
|
1623
1643
|
if (old)
|
|
1624
1644
|
this.#deliveredTextIds.delete(old);
|
|
1625
1645
|
}
|
|
1646
|
+
this.#persistDeliveredTextIds();
|
|
1647
|
+
}
|
|
1648
|
+
/** Where the delivered-id set lives between runs. Next to the friend store,
|
|
1649
|
+
* because it is the same kind of state: what this node already knows. */
|
|
1650
|
+
get #deliveredStoreFile() {
|
|
1651
|
+
return this.#friendStoreFile ? `${this.#friendStoreFile}.delivered.json` : undefined;
|
|
1652
|
+
}
|
|
1653
|
+
/** Persist the seen delivery ids.
|
|
1654
|
+
*
|
|
1655
|
+
* In memory only, this set died with the process — and the moment it did,
|
|
1656
|
+
* every message the sender had retransmitted while we were down was
|
|
1657
|
+
* accepted as new. Restart the daemon during a conversation and the last
|
|
1658
|
+
* messages arrive TWICE, which is exactly what a restart-heavy evening
|
|
1659
|
+
* looked like. The sender is behaving correctly: it never got an ACK, so
|
|
1660
|
+
* it resent. Remembering is the receiver's job.
|
|
1661
|
+
*
|
|
1662
|
+
* Debounced: a busy chat would otherwise write the file per message. */
|
|
1663
|
+
#persistDeliveredTextIds() {
|
|
1664
|
+
const file = this.#deliveredStoreFile;
|
|
1665
|
+
if (!file || this.#deliveredPersistTimer)
|
|
1666
|
+
return;
|
|
1667
|
+
this.#deliveredPersistTimer = setTimeout(() => {
|
|
1668
|
+
this.#deliveredPersistTimer = undefined;
|
|
1669
|
+
const payload = JSON.stringify(this.#deliveredTextOrder);
|
|
1670
|
+
const tmp = `${file}.tmp.${process.pid}.${this.#persistSeq++}`;
|
|
1671
|
+
void mkdir(dirname(file), { recursive: true })
|
|
1672
|
+
.then(() => writeFile(tmp, payload, "utf8"))
|
|
1673
|
+
.then(() => rename(tmp, file))
|
|
1674
|
+
.catch((error) => {
|
|
1675
|
+
this.#debugLog(`persist delivered ids failed: ${error.message}`);
|
|
1676
|
+
});
|
|
1677
|
+
}, 1000);
|
|
1678
|
+
this.#deliveredPersistTimer.unref?.();
|
|
1679
|
+
}
|
|
1680
|
+
async #loadDeliveredTextIds() {
|
|
1681
|
+
const file = this.#deliveredStoreFile;
|
|
1682
|
+
if (!file)
|
|
1683
|
+
return;
|
|
1684
|
+
try {
|
|
1685
|
+
const raw = await readFile(file, "utf8");
|
|
1686
|
+
const ids = JSON.parse(raw);
|
|
1687
|
+
if (!Array.isArray(ids))
|
|
1688
|
+
return;
|
|
1689
|
+
for (const id of ids) {
|
|
1690
|
+
if (typeof id !== "string")
|
|
1691
|
+
continue;
|
|
1692
|
+
if (this.#deliveredTextIds.has(id))
|
|
1693
|
+
continue;
|
|
1694
|
+
this.#deliveredTextIds.add(id);
|
|
1695
|
+
this.#deliveredTextOrder.push(id);
|
|
1696
|
+
}
|
|
1697
|
+
this.#debugLog(`loaded ${this.#deliveredTextIds.size} delivered text id(s)`);
|
|
1698
|
+
}
|
|
1699
|
+
catch {
|
|
1700
|
+
// Absent or unreadable: start empty. Worst case is the duplicate this
|
|
1701
|
+
// was written to prevent, not a failure to start.
|
|
1702
|
+
}
|
|
1626
1703
|
}
|
|
1627
1704
|
/** Files received inline over (bulk)messages — the iOS/C Carrier apps'
|
|
1628
1705
|
* native way of sending images/audio online (FileModel JSON envelope). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.152",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|