@decentnetwork/lan 0.1.300 → 0.1.302
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/carrier/peer-manager.d.ts +15 -0
- package/dist/carrier/peer-manager.js +36 -0
- package/dist/daemon/message-store.d.ts +3 -0
- package/dist/daemon/message-store.js +11 -0
- package/dist/daemon/server.js +46 -9
- package/dist/daemon/supervisor.d.ts +4 -0
- package/dist/daemon/supervisor.js +25 -0
- package/dist/ui/desktop/app.js +1 -1
- package/package.json +1 -1
|
@@ -226,6 +226,21 @@ export declare class PeerManager extends EventEmitter {
|
|
|
226
226
|
* through PacketSession.
|
|
227
227
|
*/
|
|
228
228
|
sendText(pubkey: string, text: string): Promise<void>;
|
|
229
|
+
/** Send a text to a friend who is NOT connected: the SDK posts it to the
|
|
230
|
+
* express store-and-forward relay, and the friend's client collects it on
|
|
231
|
+
* its next pull — the same path the browser client and the phones already
|
|
232
|
+
* use towards us. Resolves "offline" when it was posted, "acked" when a
|
|
233
|
+
* session turned out to be live after all; throws when there is no express
|
|
234
|
+
* node to post to.
|
|
235
|
+
*
|
|
236
|
+
* Two shapes, because the SDK has two send paths. A friend known to speak
|
|
237
|
+
* the delivery envelope (a JS peer, protoVersion >= 2) is sent through
|
|
238
|
+
* sendTextUntilAck with a stable deliveryId and a 250 ms window: one post,
|
|
239
|
+
* no retry loop (offline, the ACK cannot come, and the 15 s loop would post
|
|
240
|
+
* the same message to the relay three times), and the receiver de-dupes on
|
|
241
|
+
* the id if it ever sees it twice. A friend whose client is unknown or
|
|
242
|
+
* native gets the plain packet — phones do not read the envelope. */
|
|
243
|
+
sendTextOffline(pubkey: string, text: string, deliveryId: string): Promise<"offline" | "acked">;
|
|
229
244
|
/**
|
|
230
245
|
* Open an outbound packet session (initiator).
|
|
231
246
|
* Sends HANDSHAKE_REQ and waits for HANDSHAKE_ACK.
|
|
@@ -493,6 +493,42 @@ export class PeerManager extends EventEmitter {
|
|
|
493
493
|
}
|
|
494
494
|
await this.peer.sendText(pubkey, text);
|
|
495
495
|
}
|
|
496
|
+
/** Send a text to a friend who is NOT connected: the SDK posts it to the
|
|
497
|
+
* express store-and-forward relay, and the friend's client collects it on
|
|
498
|
+
* its next pull — the same path the browser client and the phones already
|
|
499
|
+
* use towards us. Resolves "offline" when it was posted, "acked" when a
|
|
500
|
+
* session turned out to be live after all; throws when there is no express
|
|
501
|
+
* node to post to.
|
|
502
|
+
*
|
|
503
|
+
* Two shapes, because the SDK has two send paths. A friend known to speak
|
|
504
|
+
* the delivery envelope (a JS peer, protoVersion >= 2) is sent through
|
|
505
|
+
* sendTextUntilAck with a stable deliveryId and a 250 ms window: one post,
|
|
506
|
+
* no retry loop (offline, the ACK cannot come, and the 15 s loop would post
|
|
507
|
+
* the same message to the relay three times), and the receiver de-dupes on
|
|
508
|
+
* the id if it ever sees it twice. A friend whose client is unknown or
|
|
509
|
+
* native gets the plain packet — phones do not read the envelope. */
|
|
510
|
+
async sendTextOffline(pubkey, text, deliveryId) {
|
|
511
|
+
if (!this.peer) {
|
|
512
|
+
throw new Error("Peer not created. Call create() first.");
|
|
513
|
+
}
|
|
514
|
+
const friend = this.peer.friends().find((f) => f.pubkey === pubkey || f.userid === pubkey);
|
|
515
|
+
if ((friend?.protoVersion ?? 0) >= 2) {
|
|
516
|
+
try {
|
|
517
|
+
await this.peer.sendTextUntilAck(pubkey, text, { deliveryId, timeoutMs: 250, retryIntervalMs: 250 });
|
|
518
|
+
return "acked";
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
// The one outcome where nothing was thrown by the send itself: the
|
|
522
|
+
// packet went out (to express, since there is no live session) and
|
|
523
|
+
// no ACK arrived in the window — which offline it cannot.
|
|
524
|
+
if (/ACK timed out/i.test(e.message))
|
|
525
|
+
return "offline";
|
|
526
|
+
throw e;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const r = await this.peer.sendText(pubkey, text);
|
|
530
|
+
return r?.delivery === "offline" ? "offline" : "acked";
|
|
531
|
+
}
|
|
496
532
|
/**
|
|
497
533
|
* Open an outbound packet session (initiator).
|
|
498
534
|
* Sends HANDSHAKE_REQ and waits for HANDSHAKE_ACK.
|
|
@@ -66,6 +66,9 @@ export declare class MessageStore {
|
|
|
66
66
|
* Used to flip a "queued" message to delivered once it's actually sent.
|
|
67
67
|
* No-op if the id isn't found or is a file entry. Returns true if patched. */
|
|
68
68
|
setStatus(peer: string, id: string, status?: "sending" | "queued" | "sent" | "failed"): boolean;
|
|
69
|
+
/** Record how a text message travelled: "online" over a live session,
|
|
70
|
+
* "offline" posted to the express relay for the friend to collect. */
|
|
71
|
+
setVia(peer: string, id: string, via: "online" | "offline"): boolean;
|
|
69
72
|
/** Outgoing messages still awaiting delivery (peer was offline), oldest
|
|
70
73
|
* first — both queued text and queued file chips. The daemon drains this
|
|
71
74
|
* on a friend's reconnect. */
|
|
@@ -74,6 +74,17 @@ export class MessageStore {
|
|
|
74
74
|
this.scheduleSave();
|
|
75
75
|
return true;
|
|
76
76
|
}
|
|
77
|
+
/** Record how a text message travelled: "online" over a live session,
|
|
78
|
+
* "offline" posted to the express relay for the friend to collect. */
|
|
79
|
+
setVia(peer, id, via) {
|
|
80
|
+
const arr = this.byPeer.get(peer);
|
|
81
|
+
const msg = arr?.find((m) => m.id === id);
|
|
82
|
+
if (!msg || msg.file)
|
|
83
|
+
return false;
|
|
84
|
+
msg.via = via;
|
|
85
|
+
this.scheduleSave();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
77
88
|
/** Outgoing messages still awaiting delivery (peer was offline), oldest
|
|
78
89
|
* first — both queued text and queued file chips. The daemon drains this
|
|
79
90
|
* on a friend's reconnect. */
|
package/dist/daemon/server.js
CHANGED
|
@@ -467,11 +467,42 @@ export class DaemonServer {
|
|
|
467
467
|
// sender in ~/.agentnet/messages.json). "queued" now means exactly
|
|
468
468
|
// one thing: nobody is sending this, the flush must.
|
|
469
469
|
const online = !!this.peerManager?.isFriendOnline(userid);
|
|
470
|
-
const msg = this.messageStore?.append(userid, "out", text, Date.now(),
|
|
470
|
+
const msg = this.messageStore?.append(userid, "out", text, Date.now(), "sending");
|
|
471
471
|
this.friendMeta?.ensure(userid);
|
|
472
472
|
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
473
|
-
if (!msg
|
|
474
|
-
|
|
473
|
+
if (!msg)
|
|
474
|
+
return;
|
|
475
|
+
// An OFFLINE friend used to get nothing at all: the message was
|
|
476
|
+
// stored "queued" and waited for a live session, which for a friend
|
|
477
|
+
// who is rarely connected — a browser tab, a phone in a pocket — can
|
|
478
|
+
// be never. Meanwhile their client, on the same SDK, posted ITS
|
|
479
|
+
// messages to the express store-and-forward relay and they arrived
|
|
480
|
+
// here as offline mail. The asymmetry was observed on 2026-09-12: a
|
|
481
|
+
// new web user's friend request and first message came through, the
|
|
482
|
+
// reply sat queued, and — because the SDK takes the first offline
|
|
483
|
+
// message from a "requested" friend as the proof of acceptance —
|
|
484
|
+
// their tab never learned it had been accepted either.
|
|
485
|
+
//
|
|
486
|
+
// So an offline friend now gets the message posted to express, the
|
|
487
|
+
// same way the SDK already does for friend requests. It is marked
|
|
488
|
+
// sent · offline, not delivered: nothing confirms it until they
|
|
489
|
+
// collect it. A live friend keeps the path below unchanged.
|
|
490
|
+
if (!online) {
|
|
491
|
+
void (async () => {
|
|
492
|
+
try {
|
|
493
|
+
const how = await this.peerManager.sendTextOffline(userid, text, `dl-${msg.id}`);
|
|
494
|
+
this.messageStore?.setStatus(userid, msg.id, undefined);
|
|
495
|
+
this.messageStore?.setVia(userid, msg.id, how === "offline" ? "offline" : "online");
|
|
496
|
+
if (how === "acked")
|
|
497
|
+
this.friendMeta?.markConfirmed(userid);
|
|
498
|
+
this.logger.info(`Text to ${userid.slice(0, 8)} ${how === "offline" ? "posted to offline mail (friend not connected)" : "delivered"}`);
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
this.logger.warn(`Offline text to ${userid.slice(0, 8)} failed, queued for reconnect: ${e.message}`);
|
|
502
|
+
this.messageStore?.setStatus(userid, msg.id, "queued");
|
|
503
|
+
}
|
|
504
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
505
|
+
})();
|
|
475
506
|
return;
|
|
476
507
|
}
|
|
477
508
|
// Deliver in the background; on failure hand it to the reconnect flush.
|
|
@@ -914,11 +945,17 @@ export class DaemonServer {
|
|
|
914
945
|
// (INBOX 2026-08-22 #2). Detect systemd by the env it stamps on
|
|
915
946
|
// every unit (sudo's env_reset strips these, so an interactive
|
|
916
947
|
// `sudo agentnet up` still takes the spawn path).
|
|
917
|
-
|
|
918
|
-
|
|
948
|
+
//
|
|
949
|
+
// launchd is the same trap on macOS: the replacement is not
|
|
950
|
+
// launchd's job, so the KeepAlive job keeps starting its own copy,
|
|
951
|
+
// which refuses the identity every ~10 s, forever (mac-dev
|
|
952
|
+
// 2026-09-14, 20934 runs). See supervisor.ts.
|
|
953
|
+
const { restartSupervisor } = await import("./supervisor.js");
|
|
954
|
+
const supervisor = restartSupervisor();
|
|
955
|
+
const underSupervisor = supervisor !== null;
|
|
919
956
|
setExitReason("selfRestart via IPC");
|
|
920
|
-
this.logger.info(
|
|
921
|
-
?
|
|
957
|
+
this.logger.info(underSupervisor
|
|
958
|
+
? `Self-restart requested via IPC — running under ${supervisor}, will exit non-zero for the supervisor to relaunch`
|
|
922
959
|
: `Self-restart requested via IPC — relaunching ${node} ${argv.join(" ")}`);
|
|
923
960
|
// Schedule shutdown for after the IPC response goes out. The
|
|
924
961
|
// 50ms timeout lets the response leave before cleanup closes IPC.
|
|
@@ -949,7 +986,7 @@ export class DaemonServer {
|
|
|
949
986
|
child.unref();
|
|
950
987
|
}
|
|
951
988
|
}
|
|
952
|
-
else if (!
|
|
989
|
+
else if (!underSupervisor) {
|
|
953
990
|
// sh -c so we can shell-quote argv safely and use sleep.
|
|
954
991
|
// Node's detached flag reparents the relauncher to PID 1.
|
|
955
992
|
// At this point stop() has released IPC, TUN and the pidfile;
|
|
@@ -969,7 +1006,7 @@ export class DaemonServer {
|
|
|
969
1006
|
// Non-zero under systemd so Restart=on-failure (and =always)
|
|
970
1007
|
// relaunches us; zero elsewhere — the detached child is the
|
|
971
1008
|
// replacement and a supervisor must not also start one.
|
|
972
|
-
setTimeout(() => process.exit(
|
|
1009
|
+
setTimeout(() => process.exit(underSupervisor ? 70 : 0), 200);
|
|
973
1010
|
})();
|
|
974
1011
|
}, 50);
|
|
975
1012
|
return { scheduledMs: 1500, argv: [node, ...argv] };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type RestartSupervisor = "systemd" | "launchd" | null;
|
|
2
|
+
/** The launchd label `agentnet service install` writes. */
|
|
3
|
+
export declare const LAUNCHD_LABEL = "com.decentlan.agentnet";
|
|
4
|
+
export declare function restartSupervisor(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): RestartSupervisor;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// supervisor.ts — who brings this daemon back after it exits?
|
|
2
|
+
//
|
|
3
|
+
// selfRestart re-execs a detached replacement everywhere except under a
|
|
4
|
+
// supervisor. Under systemd that replacement dies with the cgroup (INBOX
|
|
5
|
+
// 2026-08-22 #2). Under launchd it survives but is not launchd's job: it is
|
|
6
|
+
// reparented to pid 1, and the KeepAlive job keeps starting its own copy,
|
|
7
|
+
// which refuses the identity ("Another decentlan daemon is already running")
|
|
8
|
+
// every ~10 s, forever — 20934 runs on mac-dev by 2026-09-14, with
|
|
9
|
+
// /var/log/agentnet.log growing without bound. Under either supervisor the
|
|
10
|
+
// right restart is to exit and let the supervisor start the new code.
|
|
11
|
+
/** The launchd label `agentnet service install` writes. */
|
|
12
|
+
export const LAUNCHD_LABEL = "com.decentlan.agentnet";
|
|
13
|
+
export function restartSupervisor(platform = process.platform, env = process.env) {
|
|
14
|
+
// systemd stamps these on every unit; sudo's env_reset strips them, so an
|
|
15
|
+
// interactive `sudo agentnet up` still takes the spawn path.
|
|
16
|
+
if (platform === "linux" && (env.INVOCATION_ID || env.JOURNAL_STREAM))
|
|
17
|
+
return "systemd";
|
|
18
|
+
// launchd sets XPC_SERVICE_NAME to the job's label. A shell has "0" or its
|
|
19
|
+
// terminal app's name there, so only the installed job matches. A
|
|
20
|
+
// replacement an older build re-exec'd inherited it too — and exiting is
|
|
21
|
+
// right for that orphan as well: launchd's job takes the identity back.
|
|
22
|
+
if (platform === "darwin" && env.XPC_SERVICE_NAME === LAUNCHD_LABEL)
|
|
23
|
+
return "launchd";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.302";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.302",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|