@voidly/agent-sdk 1.8.1 → 1.9.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/dist/index.d.mts +207 -3
- package/dist/index.d.ts +207 -3
- package/dist/index.js +454 -26
- package/dist/index.mjs +453 -26
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2337,11 +2337,16 @@ async function sha256(data) {
|
|
|
2337
2337
|
}
|
|
2338
2338
|
var VoidlyAgent = class _VoidlyAgent {
|
|
2339
2339
|
constructor(identity, config) {
|
|
2340
|
+
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
2341
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
2342
|
+
this._conversations = /* @__PURE__ */ new Map();
|
|
2340
2343
|
this.did = identity.did;
|
|
2341
2344
|
this.apiKey = identity.apiKey;
|
|
2342
2345
|
this.signingKeyPair = identity.signingKeyPair;
|
|
2343
2346
|
this.encryptionKeyPair = identity.encryptionKeyPair;
|
|
2344
2347
|
this.baseUrl = config?.baseUrl || "https://api.voidly.ai";
|
|
2348
|
+
this.autoPin = config?.autoPin !== false;
|
|
2349
|
+
this.defaultRetries = config?.retries ?? 3;
|
|
2345
2350
|
}
|
|
2346
2351
|
// ─── Factory Methods ────────────────────────────────────────────────────────
|
|
2347
2352
|
/**
|
|
@@ -2408,14 +2413,23 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2408
2413
|
}
|
|
2409
2414
|
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
2410
2415
|
/**
|
|
2411
|
-
* Send an E2E encrypted message
|
|
2412
|
-
*
|
|
2416
|
+
* Send an E2E encrypted message with automatic retry and transparent TOFU.
|
|
2417
|
+
* Encryption happens locally — the relay NEVER sees plaintext or private keys.
|
|
2418
|
+
*
|
|
2419
|
+
* Features:
|
|
2420
|
+
* - **Auto-retry** with exponential backoff on transient failures
|
|
2421
|
+
* - **Transparent TOFU** — automatically pins recipient keys on first contact
|
|
2422
|
+
* - **Key verification** — warns if pinned keys have changed (potential MitM)
|
|
2413
2423
|
*/
|
|
2414
2424
|
async send(recipientDid, message, options = {}) {
|
|
2425
|
+
const maxRetries = options.retries ?? this.defaultRetries;
|
|
2415
2426
|
const profile = await this.getIdentity(recipientDid);
|
|
2416
2427
|
if (!profile) {
|
|
2417
2428
|
throw new Error(`Recipient ${recipientDid} not found`);
|
|
2418
2429
|
}
|
|
2430
|
+
if (this.autoPin && !options.skipPin) {
|
|
2431
|
+
await this._autoPinKeys(recipientDid);
|
|
2432
|
+
}
|
|
2419
2433
|
const recipientPubKey = (0, import_tweetnacl_util.decodeBase64)(profile.encryption_public_key);
|
|
2420
2434
|
const messageBytes = (0, import_tweetnacl_util.decodeUTF8)(message);
|
|
2421
2435
|
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
@@ -2431,31 +2445,36 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
2431
2445
|
ciphertext_hash: await sha256((0, import_tweetnacl_util.encodeBase64)(ciphertext))
|
|
2432
2446
|
});
|
|
2433
2447
|
const signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2448
|
+
const payload = {
|
|
2449
|
+
to: recipientDid,
|
|
2450
|
+
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
2451
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
2452
|
+
signature: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
2453
|
+
envelope: envelopeData,
|
|
2454
|
+
content_type: options.contentType || "text/plain",
|
|
2455
|
+
message_type: options.messageType || "text",
|
|
2456
|
+
thread_id: options.threadId,
|
|
2457
|
+
reply_to: options.replyTo,
|
|
2458
|
+
ttl: options.ttl
|
|
2459
|
+
};
|
|
2460
|
+
const raw = await this._fetchWithRetry(
|
|
2461
|
+
`${this.baseUrl}/v1/agent/send/encrypted`,
|
|
2462
|
+
{
|
|
2463
|
+
method: "POST",
|
|
2464
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
2465
|
+
body: JSON.stringify(payload)
|
|
2439
2466
|
},
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
ttl: options.ttl
|
|
2452
|
-
})
|
|
2453
|
-
});
|
|
2454
|
-
if (!res.ok) {
|
|
2455
|
-
const err = await res.json().catch(() => ({}));
|
|
2456
|
-
throw new Error(`Send failed: ${err.error?.message || err.error || res.statusText}`);
|
|
2457
|
-
}
|
|
2458
|
-
return await res.json();
|
|
2467
|
+
{ maxRetries, baseDelay: 500, maxDelay: 1e4 }
|
|
2468
|
+
);
|
|
2469
|
+
return {
|
|
2470
|
+
id: raw.id,
|
|
2471
|
+
from: raw.from,
|
|
2472
|
+
to: raw.to,
|
|
2473
|
+
timestamp: raw.timestamp,
|
|
2474
|
+
expiresAt: raw.expires_at || raw.expiresAt,
|
|
2475
|
+
encrypted: raw.encrypted,
|
|
2476
|
+
clientSide: raw.client_side || raw.clientSide
|
|
2477
|
+
};
|
|
2459
2478
|
}
|
|
2460
2479
|
/**
|
|
2461
2480
|
* Receive and decrypt messages. Decryption happens locally.
|
|
@@ -3520,6 +3539,413 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
3520
3539
|
if (!res.ok) throw new Error(`Key verify failed: ${res.status}`);
|
|
3521
3540
|
return res.json();
|
|
3522
3541
|
}
|
|
3542
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3543
|
+
// LISTEN — Event-Driven Message Receiving
|
|
3544
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3545
|
+
/**
|
|
3546
|
+
* Listen for incoming messages with an event-driven callback.
|
|
3547
|
+
* Uses adaptive polling — speeds up when messages are flowing, slows down when idle.
|
|
3548
|
+
* Automatically sends heartbeat pings to signal the agent is online.
|
|
3549
|
+
*
|
|
3550
|
+
* @example
|
|
3551
|
+
* ```ts
|
|
3552
|
+
* // Simple listener
|
|
3553
|
+
* const handle = agent.listen((msg) => {
|
|
3554
|
+
* console.log(`${msg.from}: ${msg.content}`);
|
|
3555
|
+
* });
|
|
3556
|
+
*
|
|
3557
|
+
* // Stop after 60 seconds
|
|
3558
|
+
* setTimeout(() => handle.stop(), 60000);
|
|
3559
|
+
*
|
|
3560
|
+
* // With options
|
|
3561
|
+
* const handle = agent.listen(
|
|
3562
|
+
* (msg) => console.log(msg.content),
|
|
3563
|
+
* {
|
|
3564
|
+
* interval: 1000, // poll every 1s
|
|
3565
|
+
* from: 'did:voidly:x', // only from this agent
|
|
3566
|
+
* threadId: 'conv-1', // only this thread
|
|
3567
|
+
* adaptive: true, // slow down when idle
|
|
3568
|
+
* heartbeat: true, // send pings
|
|
3569
|
+
* }
|
|
3570
|
+
* );
|
|
3571
|
+
* ```
|
|
3572
|
+
*/
|
|
3573
|
+
listen(onMessage, options = {}, onError) {
|
|
3574
|
+
const interval = Math.max(options.interval || 2e3, 500);
|
|
3575
|
+
const adaptive = options.adaptive !== false;
|
|
3576
|
+
const autoMarkRead = options.autoMarkRead !== false;
|
|
3577
|
+
const unreadOnly = options.unreadOnly !== false;
|
|
3578
|
+
const heartbeat = options.heartbeat !== false;
|
|
3579
|
+
const heartbeatInterval = options.heartbeatInterval || 6e4;
|
|
3580
|
+
let active = true;
|
|
3581
|
+
let currentInterval = interval;
|
|
3582
|
+
let consecutiveEmpty = 0;
|
|
3583
|
+
let lastSeen;
|
|
3584
|
+
let timer = null;
|
|
3585
|
+
let heartbeatTimer = null;
|
|
3586
|
+
const handle = {
|
|
3587
|
+
stop: () => {
|
|
3588
|
+
active = false;
|
|
3589
|
+
if (timer) clearTimeout(timer);
|
|
3590
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
3591
|
+
this._listeners.delete(handle);
|
|
3592
|
+
},
|
|
3593
|
+
get active() {
|
|
3594
|
+
return active;
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
this._listeners.add(handle);
|
|
3598
|
+
if (heartbeat) {
|
|
3599
|
+
heartbeatTimer = setInterval(async () => {
|
|
3600
|
+
if (!active) return;
|
|
3601
|
+
try {
|
|
3602
|
+
await this.ping();
|
|
3603
|
+
} catch {
|
|
3604
|
+
}
|
|
3605
|
+
}, heartbeatInterval);
|
|
3606
|
+
this.ping().catch(() => {
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
const poll = async () => {
|
|
3610
|
+
if (!active || options.signal?.aborted) {
|
|
3611
|
+
handle.stop();
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
try {
|
|
3615
|
+
const messages = await this.receive({
|
|
3616
|
+
since: lastSeen,
|
|
3617
|
+
from: options.from,
|
|
3618
|
+
threadId: options.threadId,
|
|
3619
|
+
messageType: options.messageType,
|
|
3620
|
+
unreadOnly,
|
|
3621
|
+
limit: 50
|
|
3622
|
+
});
|
|
3623
|
+
if (messages.length > 0) {
|
|
3624
|
+
consecutiveEmpty = 0;
|
|
3625
|
+
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
3626
|
+
for (const msg of messages) {
|
|
3627
|
+
try {
|
|
3628
|
+
await onMessage(msg);
|
|
3629
|
+
if (autoMarkRead) {
|
|
3630
|
+
await this.markRead(msg.id).catch(() => {
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
} catch (err) {
|
|
3634
|
+
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
lastSeen = messages[messages.length - 1].timestamp;
|
|
3638
|
+
} else {
|
|
3639
|
+
consecutiveEmpty++;
|
|
3640
|
+
if (adaptive && consecutiveEmpty > 3) {
|
|
3641
|
+
currentInterval = Math.min(currentInterval * 1.5, interval * 4);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
} catch (err) {
|
|
3645
|
+
if (onError) onError(err instanceof Error ? err : new Error(String(err)));
|
|
3646
|
+
currentInterval = Math.min(currentInterval * 2, interval * 8);
|
|
3647
|
+
}
|
|
3648
|
+
if (active && !options.signal?.aborted) {
|
|
3649
|
+
timer = setTimeout(poll, currentInterval);
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
poll();
|
|
3653
|
+
return handle;
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Listen for messages as an async iterator.
|
|
3657
|
+
* Enables `for await` syntax for message processing.
|
|
3658
|
+
*
|
|
3659
|
+
* @example
|
|
3660
|
+
* ```ts
|
|
3661
|
+
* for await (const msg of agent.messages({ unreadOnly: true })) {
|
|
3662
|
+
* console.log(`${msg.from}: ${msg.content}`);
|
|
3663
|
+
* if (msg.content === 'quit') break;
|
|
3664
|
+
* }
|
|
3665
|
+
* ```
|
|
3666
|
+
*/
|
|
3667
|
+
async *messages(options = {}) {
|
|
3668
|
+
const queue = [];
|
|
3669
|
+
let resolve = null;
|
|
3670
|
+
let done = false;
|
|
3671
|
+
const handle = this.listen(
|
|
3672
|
+
(msg) => {
|
|
3673
|
+
queue.push(msg);
|
|
3674
|
+
if (resolve) {
|
|
3675
|
+
resolve();
|
|
3676
|
+
resolve = null;
|
|
3677
|
+
}
|
|
3678
|
+
},
|
|
3679
|
+
{ ...options, autoMarkRead: options.autoMarkRead !== false }
|
|
3680
|
+
);
|
|
3681
|
+
options.signal?.addEventListener("abort", () => {
|
|
3682
|
+
done = true;
|
|
3683
|
+
handle.stop();
|
|
3684
|
+
if (resolve) {
|
|
3685
|
+
resolve();
|
|
3686
|
+
resolve = null;
|
|
3687
|
+
}
|
|
3688
|
+
});
|
|
3689
|
+
try {
|
|
3690
|
+
while (!done && !options.signal?.aborted) {
|
|
3691
|
+
if (queue.length > 0) {
|
|
3692
|
+
yield queue.shift();
|
|
3693
|
+
} else {
|
|
3694
|
+
await new Promise((r) => {
|
|
3695
|
+
resolve = r;
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
} finally {
|
|
3700
|
+
handle.stop();
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
/**
|
|
3704
|
+
* Stop all active listeners. Useful for clean shutdown.
|
|
3705
|
+
*/
|
|
3706
|
+
stopAll() {
|
|
3707
|
+
for (const listener of this._listeners) {
|
|
3708
|
+
listener.stop();
|
|
3709
|
+
}
|
|
3710
|
+
this._listeners.clear();
|
|
3711
|
+
}
|
|
3712
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3713
|
+
// CONVERSATIONS — Thread Management
|
|
3714
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3715
|
+
/**
|
|
3716
|
+
* Start or resume a conversation with another agent.
|
|
3717
|
+
* Automatically manages thread IDs, message history, and reply chains.
|
|
3718
|
+
*
|
|
3719
|
+
* @example
|
|
3720
|
+
* ```ts
|
|
3721
|
+
* const conv = agent.conversation(otherDid);
|
|
3722
|
+
* await conv.say('Hello!');
|
|
3723
|
+
* await conv.say('How are you?');
|
|
3724
|
+
*
|
|
3725
|
+
* // Get full history
|
|
3726
|
+
* const history = await conv.history();
|
|
3727
|
+
*
|
|
3728
|
+
* // Listen for replies in this conversation
|
|
3729
|
+
* conv.onReply((msg) => {
|
|
3730
|
+
* console.log(`Reply: ${msg.content}`);
|
|
3731
|
+
* });
|
|
3732
|
+
* ```
|
|
3733
|
+
*/
|
|
3734
|
+
conversation(peerDid, threadId) {
|
|
3735
|
+
const tid = threadId || `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3736
|
+
const key = `${peerDid}:${tid}`;
|
|
3737
|
+
if (this._conversations.has(key)) {
|
|
3738
|
+
return this._conversations.get(key);
|
|
3739
|
+
}
|
|
3740
|
+
const conv = new Conversation(this, peerDid, tid);
|
|
3741
|
+
this._conversations.set(key, conv);
|
|
3742
|
+
return conv;
|
|
3743
|
+
}
|
|
3744
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3745
|
+
// INTERNAL — Retry, Auto-Pin
|
|
3746
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3747
|
+
/** @internal Auto-pin keys on first contact (TOFU) */
|
|
3748
|
+
async _autoPinKeys(did) {
|
|
3749
|
+
if (this._pinnedDids.has(did)) return;
|
|
3750
|
+
this._pinnedDids.add(did);
|
|
3751
|
+
try {
|
|
3752
|
+
const result = await this.pinKeys(did);
|
|
3753
|
+
if (result.key_changed && result.warning) {
|
|
3754
|
+
console.warn(`[voidly] \u26A0 Key change detected for ${did}: ${result.warning}`);
|
|
3755
|
+
}
|
|
3756
|
+
} catch {
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
/** @internal Fetch with exponential backoff retry */
|
|
3760
|
+
async _fetchWithRetry(url, init, retry = {}) {
|
|
3761
|
+
const maxRetries = retry.maxRetries ?? 3;
|
|
3762
|
+
const baseDelay = retry.baseDelay ?? 500;
|
|
3763
|
+
const maxDelay = retry.maxDelay ?? 1e4;
|
|
3764
|
+
let lastError = null;
|
|
3765
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
3766
|
+
try {
|
|
3767
|
+
const res = await fetch(url, init);
|
|
3768
|
+
if (res.ok) {
|
|
3769
|
+
return await res.json();
|
|
3770
|
+
}
|
|
3771
|
+
const err = await res.json().catch(() => ({}));
|
|
3772
|
+
const errMsg = err.error?.message || err.error || res.statusText;
|
|
3773
|
+
if (res.status >= 400 && res.status < 500) {
|
|
3774
|
+
throw new Error(`Send failed (${res.status}): ${errMsg}`);
|
|
3775
|
+
}
|
|
3776
|
+
lastError = new Error(`Send failed (${res.status}): ${errMsg}`);
|
|
3777
|
+
} catch (err) {
|
|
3778
|
+
if (err instanceof Error && err.message.startsWith("Send failed (4")) {
|
|
3779
|
+
throw err;
|
|
3780
|
+
}
|
|
3781
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
3782
|
+
}
|
|
3783
|
+
if (attempt < maxRetries) {
|
|
3784
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
3785
|
+
const jitter = delay * (0.5 + Math.random() * 0.5);
|
|
3786
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
throw lastError || new Error("Send failed after retries");
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
var Conversation = class {
|
|
3793
|
+
/** @internal */
|
|
3794
|
+
constructor(agent, peerDid, threadId) {
|
|
3795
|
+
this._lastMessageId = null;
|
|
3796
|
+
this._messageHistory = [];
|
|
3797
|
+
this._listener = null;
|
|
3798
|
+
this._replyHandlers = [];
|
|
3799
|
+
this.agent = agent;
|
|
3800
|
+
this.peerDid = peerDid;
|
|
3801
|
+
this.threadId = threadId;
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Send a message in this conversation. Auto-threaded and auto-linked to previous message.
|
|
3805
|
+
*/
|
|
3806
|
+
async say(content, options) {
|
|
3807
|
+
const result = await this.agent.send(this.peerDid, content, {
|
|
3808
|
+
...options,
|
|
3809
|
+
threadId: this.threadId,
|
|
3810
|
+
replyTo: this._lastMessageId || void 0
|
|
3811
|
+
});
|
|
3812
|
+
this._lastMessageId = result.id;
|
|
3813
|
+
this._messageHistory.push({
|
|
3814
|
+
id: result.id,
|
|
3815
|
+
from: this.agent.did,
|
|
3816
|
+
content,
|
|
3817
|
+
timestamp: result.timestamp,
|
|
3818
|
+
signatureValid: true,
|
|
3819
|
+
messageType: options?.messageType || "text"
|
|
3820
|
+
});
|
|
3821
|
+
return result;
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Get conversation history (both sent and received messages in this thread).
|
|
3825
|
+
*/
|
|
3826
|
+
async history(options) {
|
|
3827
|
+
const received = await this.agent.receive({
|
|
3828
|
+
threadId: this.threadId,
|
|
3829
|
+
from: this.peerDid,
|
|
3830
|
+
limit: options?.limit || 100
|
|
3831
|
+
});
|
|
3832
|
+
const all = [
|
|
3833
|
+
...this._messageHistory,
|
|
3834
|
+
...received.map((m) => ({
|
|
3835
|
+
id: m.id,
|
|
3836
|
+
from: m.from,
|
|
3837
|
+
content: m.content,
|
|
3838
|
+
timestamp: m.timestamp,
|
|
3839
|
+
signatureValid: m.signatureValid,
|
|
3840
|
+
messageType: m.messageType
|
|
3841
|
+
}))
|
|
3842
|
+
];
|
|
3843
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3844
|
+
return all.filter((m) => {
|
|
3845
|
+
if (seen.has(m.id)) return false;
|
|
3846
|
+
seen.add(m.id);
|
|
3847
|
+
return true;
|
|
3848
|
+
}).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
3849
|
+
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Register a callback for replies in this conversation.
|
|
3852
|
+
*/
|
|
3853
|
+
onReply(handler) {
|
|
3854
|
+
this._replyHandlers.push(handler);
|
|
3855
|
+
if (!this._listener) {
|
|
3856
|
+
this._listener = this.agent.listen(
|
|
3857
|
+
async (msg) => {
|
|
3858
|
+
this._messageHistory.push({
|
|
3859
|
+
id: msg.id,
|
|
3860
|
+
from: msg.from,
|
|
3861
|
+
content: msg.content,
|
|
3862
|
+
timestamp: msg.timestamp,
|
|
3863
|
+
signatureValid: msg.signatureValid,
|
|
3864
|
+
messageType: msg.messageType
|
|
3865
|
+
});
|
|
3866
|
+
this._lastMessageId = msg.id;
|
|
3867
|
+
for (const h of this._replyHandlers) {
|
|
3868
|
+
try {
|
|
3869
|
+
await h(msg);
|
|
3870
|
+
} catch {
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
},
|
|
3874
|
+
{ from: this.peerDid, threadId: this.threadId, autoMarkRead: true }
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Wait for the next reply in this conversation (Promise-based).
|
|
3880
|
+
*
|
|
3881
|
+
* @param timeoutMs - Maximum time to wait (default: 30000ms)
|
|
3882
|
+
* @throws Error on timeout
|
|
3883
|
+
*/
|
|
3884
|
+
async waitForReply(timeoutMs = 3e4) {
|
|
3885
|
+
return new Promise((resolve, reject) => {
|
|
3886
|
+
let resolved = false;
|
|
3887
|
+
const timeout = setTimeout(() => {
|
|
3888
|
+
if (!resolved) {
|
|
3889
|
+
resolved = true;
|
|
3890
|
+
reject(new Error(`No reply received within ${timeoutMs}ms`));
|
|
3891
|
+
}
|
|
3892
|
+
}, timeoutMs);
|
|
3893
|
+
const check = async () => {
|
|
3894
|
+
while (!resolved) {
|
|
3895
|
+
const messages = await this.agent.receive({
|
|
3896
|
+
from: this.peerDid,
|
|
3897
|
+
threadId: this.threadId,
|
|
3898
|
+
unreadOnly: true,
|
|
3899
|
+
limit: 1
|
|
3900
|
+
});
|
|
3901
|
+
if (messages.length > 0 && !resolved) {
|
|
3902
|
+
resolved = true;
|
|
3903
|
+
clearTimeout(timeout);
|
|
3904
|
+
const msg = messages[0];
|
|
3905
|
+
this._messageHistory.push({
|
|
3906
|
+
id: msg.id,
|
|
3907
|
+
from: msg.from,
|
|
3908
|
+
content: msg.content,
|
|
3909
|
+
timestamp: msg.timestamp,
|
|
3910
|
+
signatureValid: msg.signatureValid,
|
|
3911
|
+
messageType: msg.messageType
|
|
3912
|
+
});
|
|
3913
|
+
this._lastMessageId = msg.id;
|
|
3914
|
+
await this.agent.markRead(msg.id).catch(() => {
|
|
3915
|
+
});
|
|
3916
|
+
resolve(msg);
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3920
|
+
}
|
|
3921
|
+
};
|
|
3922
|
+
check().catch((err) => {
|
|
3923
|
+
if (!resolved) {
|
|
3924
|
+
resolved = true;
|
|
3925
|
+
clearTimeout(timeout);
|
|
3926
|
+
reject(err);
|
|
3927
|
+
}
|
|
3928
|
+
});
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
/**
|
|
3932
|
+
* Stop listening for replies and clean up.
|
|
3933
|
+
*/
|
|
3934
|
+
close() {
|
|
3935
|
+
if (this._listener) {
|
|
3936
|
+
this._listener.stop();
|
|
3937
|
+
this._listener = null;
|
|
3938
|
+
}
|
|
3939
|
+
this._replyHandlers = [];
|
|
3940
|
+
}
|
|
3941
|
+
/** Number of messages tracked locally */
|
|
3942
|
+
get length() {
|
|
3943
|
+
return this._messageHistory.length;
|
|
3944
|
+
}
|
|
3945
|
+
/** The last message in this conversation */
|
|
3946
|
+
get lastMessage() {
|
|
3947
|
+
return this._messageHistory.length > 0 ? this._messageHistory[this._messageHistory.length - 1] : null;
|
|
3948
|
+
}
|
|
3523
3949
|
};
|
|
3524
3950
|
var export_decodeBase64 = import_tweetnacl_util.decodeBase64;
|
|
3525
3951
|
var export_decodeUTF8 = import_tweetnacl_util.decodeUTF8;
|
|
@@ -3527,6 +3953,7 @@ var export_encodeBase64 = import_tweetnacl_util.encodeBase64;
|
|
|
3527
3953
|
var export_encodeUTF8 = import_tweetnacl_util.encodeUTF8;
|
|
3528
3954
|
var export_nacl = import_tweetnacl.default;
|
|
3529
3955
|
export {
|
|
3956
|
+
Conversation,
|
|
3530
3957
|
VoidlyAgent,
|
|
3531
3958
|
export_decodeBase64 as decodeBase64,
|
|
3532
3959
|
export_decodeUTF8 as decodeUTF8,
|