@xmtp/node-sdk 6.1.0-rc1 → 6.2.0-dev.43f235e
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 +23 -3
- package/dist/index.d.ts +383 -280
- package/dist/index.js +1158 -498
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1,72 +1,20 @@
|
|
|
1
|
-
import { BackendBuilder, generateInboxId as generateInboxId$1, getInboxIdByIdentity, contentTypeIntent, contentTypeActions, contentTypeWalletSendCalls, contentTypeLeaveRequest, contentTypeReadReceipt, contentTypeGroupUpdated, contentTypeTransactionReference, contentTypeMultiRemoteAttachment, contentTypeRemoteAttachment, contentTypeAttachment, contentTypeReaction, contentTypeReply, contentTypeMarkdown, contentTypeText, createClientWithBackend, revokeInstallationsSignatureRequest, applySignatureRequest, fetchInboxStatesByInboxIds, verifySignedWithPublicKey, isAddressAuthorized, isInstallationAuthorized, Backend } from '@xmtp/node-bindings';
|
|
1
|
+
import { BackendBuilder, generateInboxId as generateInboxId$1, getInboxIdByIdentity, contentTypeIntent, contentTypeActions, contentTypeWalletSendCalls, contentTypeLeaveRequest, contentTypeReadReceipt, contentTypeGroupUpdated, contentTypeTransactionReference, contentTypeMultiRemoteAttachment, contentTypeRemoteAttachment, contentTypeAttachment, contentTypeReaction, contentTypeReply, contentTypeMarkdown, contentTypeText, UnstableChangeCallbacks, createClientWithBackend, revokeInstallationsSignatureRequest, applySignatureRequest, fetchInboxStatesByInboxIds, verifySignedWithPublicKey, isAddressAuthorized, isInstallationAuthorized, fetchServerConfiguration, Backend } from '@xmtp/node-bindings';
|
|
2
2
|
export { ActionStyle, BackupElementSelectionOption, ConsentEntityType, ConsentState, ContentType, ConversationType, DeliveryStatus, GroupMembershipState, GroupMessageKind, GroupPermissionsOptions, IdentifierKind, ListConversationsOrderBy, LogLevel, MessageSortBy, MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, ReactionAction, ReactionSchema, SortDirection, WorkerKind, contentTypeActions, contentTypeAttachment, contentTypeGroupUpdated, contentTypeIntent, contentTypeLeaveRequest, contentTypeMarkdown, contentTypeMultiRemoteAttachment, contentTypeReaction, contentTypeReadReceipt, contentTypeRemoteAttachment, contentTypeReply, contentTypeText, contentTypeTransactionReference, contentTypeWalletSendCalls, decryptAttachment, encodeActions, encodeAttachment, encodeIntent, encodeMarkdown, encodeMultiRemoteAttachment, encodeReaction, encodeReadReceipt, encodeRemoteAttachment, encodeText, encodeTransactionReference, encodeWalletSendCalls, encryptAttachment, flushTelemetry, initLogging } from '@xmtp/node-bindings';
|
|
3
|
-
import { randomBytes } from 'node:crypto';
|
|
4
3
|
import { contentTypeToString } from '@xmtp/content-type-primitives';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
5
|
import { isPromise } from 'node:util/types';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import process from 'node:process';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Pre-configured URLs for the XMTP network based on the environment
|
|
11
|
-
*
|
|
12
|
-
* @deprecated Use `createBackend()` instead.
|
|
13
|
-
* @constant
|
|
14
|
-
* @property {string} local - The local URL for the XMTP network
|
|
15
|
-
* @property {string} dev - The development URL for the XMTP network
|
|
16
|
-
* @property {string} production - The production URL for the XMTP network
|
|
17
|
-
*/
|
|
18
|
-
const ApiUrls = {
|
|
19
|
-
local: "http://localhost:5556",
|
|
20
|
-
dev: "https://grpc.dev.xmtp.network:443",
|
|
21
|
-
production: "https://grpc.production.xmtp.network:443",
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Pre-configured URLs for the XMTP history sync service based on the environment
|
|
25
|
-
*
|
|
26
|
-
* @constant
|
|
27
|
-
* @property {string} local - The local URL for the XMTP history sync service
|
|
28
|
-
* @property {string} dev - The development URL for the XMTP history sync service
|
|
29
|
-
* @property {string} production - The production URL for the XMTP history sync service
|
|
30
|
-
*/
|
|
31
|
-
const HistorySyncUrls = {
|
|
32
|
-
local: "http://localhost:5558",
|
|
33
|
-
dev: "https://message-history.dev.ephemera.network",
|
|
34
|
-
production: "https://message-history.production.ephemera.network",
|
|
35
|
-
"testnet-staging": "https://message-history.dev.ephemera.network",
|
|
36
|
-
"testnet-dev": "https://message-history.dev.ephemera.network",
|
|
37
|
-
testnet: "https://message-history.dev.ephemera.network",
|
|
38
|
-
mainnet: "https://message-history.production.ephemera.network",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const envMap = {
|
|
42
|
-
local: "Local" /* BindingsEnv.Local */,
|
|
43
|
-
dev: "Dev" /* BindingsEnv.Dev */,
|
|
44
|
-
production: "Production" /* BindingsEnv.Production */,
|
|
45
|
-
"testnet-staging": "TestnetStaging" /* BindingsEnv.TestnetStaging */,
|
|
46
|
-
"testnet-dev": "TestnetDev" /* BindingsEnv.TestnetDev */,
|
|
47
|
-
testnet: "Testnet" /* BindingsEnv.Testnet */,
|
|
48
|
-
mainnet: "Mainnet" /* BindingsEnv.Mainnet */,
|
|
49
|
-
};
|
|
50
|
-
const reverseEnvMap = {
|
|
51
|
-
["Local" /* BindingsEnv.Local */]: "local",
|
|
52
|
-
["Dev" /* BindingsEnv.Dev */]: "dev",
|
|
53
|
-
["Production" /* BindingsEnv.Production */]: "production",
|
|
54
|
-
["TestnetStaging" /* BindingsEnv.TestnetStaging */]: "testnet-staging",
|
|
55
|
-
["TestnetDev" /* BindingsEnv.TestnetDev */]: "testnet-dev",
|
|
56
|
-
["Testnet" /* BindingsEnv.Testnet */]: "testnet",
|
|
57
|
-
["Mainnet" /* BindingsEnv.Mainnet */]: "mainnet",
|
|
58
|
-
};
|
|
59
|
-
const envToString = (env) => {
|
|
60
|
-
return reverseEnvMap[env];
|
|
61
|
-
};
|
|
62
9
|
const createBackend = async (options) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Validate options from JavaScript callers.
|
|
11
|
+
if (!options?.backendUrl?.trim()) {
|
|
12
|
+
throw new Error("backendUrl is required");
|
|
13
|
+
}
|
|
14
|
+
const builder = new BackendBuilder(options.backendUrl);
|
|
15
|
+
if (options.env !== undefined)
|
|
16
|
+
builder.setEnv(options.env);
|
|
17
|
+
if (options.appVersion !== undefined)
|
|
70
18
|
builder.setAppVersion(options.appVersion);
|
|
71
19
|
return builder.build();
|
|
72
20
|
};
|
|
@@ -173,6 +121,13 @@ function nsToDate(ns) {
|
|
|
173
121
|
return new Date(Number(ns / 1000000n));
|
|
174
122
|
}
|
|
175
123
|
|
|
124
|
+
const decodeFailures = new WeakMap();
|
|
125
|
+
/** Reject failed registered codecs at delivery without changing legacy reads. */
|
|
126
|
+
const assertMessageDecodedForDelivery = (message) => {
|
|
127
|
+
const failure = decodeFailures.get(message);
|
|
128
|
+
if (failure)
|
|
129
|
+
throw failure.error;
|
|
130
|
+
};
|
|
176
131
|
const getContentFromDecodedMessageContent = (content) => {
|
|
177
132
|
switch (content.type) {
|
|
178
133
|
case "Text" /* DecodedMessageContentType.Text */: {
|
|
@@ -303,6 +258,8 @@ const getContentTypeFromDecodedMessageContent = (content) => {
|
|
|
303
258
|
* @property {bigint} sentAtNs - Timestamp when the message was sent (in nanoseconds)
|
|
304
259
|
*/
|
|
305
260
|
class DecodedMessage {
|
|
261
|
+
/** Database-local position of this delivery. Absent for ordinary history reads. */
|
|
262
|
+
deliveryCursor;
|
|
306
263
|
content;
|
|
307
264
|
contentType;
|
|
308
265
|
conversationId;
|
|
@@ -331,8 +288,15 @@ class DecodedMessage {
|
|
|
331
288
|
this.fallback = message.fallback ?? undefined;
|
|
332
289
|
this.kind = message.kind;
|
|
333
290
|
this.deliveryStatus = message.deliveryStatus;
|
|
291
|
+
const decodeChild = (child) => {
|
|
292
|
+
const decoded = new DecodedMessage(codecRegistry, child);
|
|
293
|
+
const failure = decodeFailures.get(decoded);
|
|
294
|
+
if (failure && !decodeFailures.has(this))
|
|
295
|
+
decodeFailures.set(this, failure);
|
|
296
|
+
return decoded;
|
|
297
|
+
};
|
|
334
298
|
this.numReplies = message.numReplies;
|
|
335
|
-
this.reactions = message.reactions.map((reaction) =>
|
|
299
|
+
this.reactions = message.reactions.map((reaction) => decodeChild(reaction));
|
|
336
300
|
this.content =
|
|
337
301
|
getContentFromDecodedMessageContent(message.content) ??
|
|
338
302
|
undefined;
|
|
@@ -347,6 +311,8 @@ class DecodedMessage {
|
|
|
347
311
|
replyContent = codec.decode(replyContent);
|
|
348
312
|
}
|
|
349
313
|
catch (error) {
|
|
314
|
+
if (!decodeFailures.has(this))
|
|
315
|
+
decodeFailures.set(this, { error });
|
|
350
316
|
if (error instanceof Error) {
|
|
351
317
|
console.warn(`Error decoding custom content: ${error.message}`);
|
|
352
318
|
}
|
|
@@ -361,7 +327,7 @@ class DecodedMessage {
|
|
|
361
327
|
content: replyContent,
|
|
362
328
|
contentType: getContentTypeFromDecodedMessageContent(reply.content),
|
|
363
329
|
inReplyTo: reply.inReplyTo
|
|
364
|
-
?
|
|
330
|
+
? decodeChild(reply.inReplyTo)
|
|
365
331
|
: null,
|
|
366
332
|
};
|
|
367
333
|
break;
|
|
@@ -375,6 +341,8 @@ class DecodedMessage {
|
|
|
375
341
|
this.content = codec.decode(customContent);
|
|
376
342
|
}
|
|
377
343
|
catch (error) {
|
|
344
|
+
if (!decodeFailures.has(this))
|
|
345
|
+
decodeFailures.set(this, { error });
|
|
378
346
|
if (error instanceof Error) {
|
|
379
347
|
console.warn(`Error decoding custom content: ${error.message}`);
|
|
380
348
|
}
|
|
@@ -395,292 +363,236 @@ class DecodedMessage {
|
|
|
395
363
|
}
|
|
396
364
|
}
|
|
397
365
|
|
|
398
|
-
/**
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
* }
|
|
418
|
-
* ```
|
|
419
|
-
*/
|
|
420
|
-
class AsyncStream {
|
|
421
|
-
isDone = false;
|
|
422
|
-
#pendingResolves = [];
|
|
423
|
-
#queue;
|
|
424
|
-
onDone;
|
|
425
|
-
onReturn;
|
|
426
|
-
constructor() {
|
|
427
|
-
this.#queue = [];
|
|
428
|
-
this.isDone = false;
|
|
429
|
-
}
|
|
430
|
-
flush() {
|
|
431
|
-
while (this.#pendingResolves.length > 0) {
|
|
432
|
-
const nextResolve = this.#pendingResolves.shift();
|
|
433
|
-
if (nextResolve) {
|
|
434
|
-
nextResolve({ done: true, value: undefined });
|
|
435
|
-
}
|
|
366
|
+
/** One pending item. Construction selects callback mode or next-request acknowledgement. */
|
|
367
|
+
class MessageStream {
|
|
368
|
+
#reader;
|
|
369
|
+
#convert;
|
|
370
|
+
#options;
|
|
371
|
+
#onValue;
|
|
372
|
+
#pending;
|
|
373
|
+
#reading = false;
|
|
374
|
+
#done = false;
|
|
375
|
+
#closing;
|
|
376
|
+
#cursor;
|
|
377
|
+
constructor(reader, convert, options = {}) {
|
|
378
|
+
this.#reader = reader;
|
|
379
|
+
this.#convert = convert;
|
|
380
|
+
this.#options = options;
|
|
381
|
+
this.#onValue = options.onValue;
|
|
382
|
+
if (this.#onValue) {
|
|
383
|
+
// Callback mode has no caller waiting on next(). Errors reach onError before cleanup.
|
|
384
|
+
void this.#read().catch(() => undefined);
|
|
436
385
|
}
|
|
437
386
|
}
|
|
438
|
-
|
|
439
|
-
this
|
|
440
|
-
this.#queue = [];
|
|
441
|
-
this.#pendingResolves = [];
|
|
442
|
-
this.isDone = true;
|
|
443
|
-
this.onDone?.();
|
|
387
|
+
get isDone() {
|
|
388
|
+
return this.#done;
|
|
444
389
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
390
|
+
get deliveredCursor() {
|
|
391
|
+
return this.#cursor;
|
|
392
|
+
}
|
|
393
|
+
#hasEnded() {
|
|
394
|
+
return this.#done;
|
|
395
|
+
}
|
|
396
|
+
next = () => {
|
|
397
|
+
if (this.#onValue) {
|
|
398
|
+
return Promise.reject(new Error("Cannot call next() on a message stream in callback mode"));
|
|
448
399
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
400
|
+
return this.#read();
|
|
401
|
+
};
|
|
402
|
+
async #acknowledgePending() {
|
|
403
|
+
const pending = this.#pending;
|
|
404
|
+
if (pending) {
|
|
405
|
+
// Keep the token until success so close can reject a failed acknowledgement.
|
|
406
|
+
await pending.acknowledge();
|
|
407
|
+
if (this.#pending === pending)
|
|
408
|
+
this.#pending = undefined;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async #read() {
|
|
412
|
+
if (this.#hasEnded())
|
|
413
|
+
return { done: true, value: undefined };
|
|
414
|
+
if (this.#reading)
|
|
415
|
+
throw new Error("A message read is already pending");
|
|
416
|
+
this.#reading = true;
|
|
417
|
+
try {
|
|
418
|
+
await this.#acknowledgePending();
|
|
419
|
+
while (!this.#hasEnded()) {
|
|
420
|
+
const item = await this.#reader.nextDelivery();
|
|
421
|
+
if (item === undefined || item === null) {
|
|
422
|
+
await this.return();
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (this.#hasEnded()) {
|
|
426
|
+
await item.acknowledgement.reject();
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
this.#pending = item.acknowledgement;
|
|
430
|
+
// A codec that throws must not advance the cursor. The message is
|
|
431
|
+
// retained and a later reader, or the same client with the codec
|
|
432
|
+
// registered, must still see it. Acknowledging here would skip a
|
|
433
|
+
// message nothing has read. This matches the undefined case below:
|
|
434
|
+
// both mean this client could not decode the message, so both hold
|
|
435
|
+
// the item rather than discarding it.
|
|
436
|
+
const value = await this.#convert(item.message, item.cursor);
|
|
437
|
+
if (this.#hasEnded())
|
|
438
|
+
break;
|
|
439
|
+
const checked = item.acknowledgement.checkOwner();
|
|
440
|
+
const valid = typeof checked === "boolean" ? checked : await checked;
|
|
441
|
+
if (this.#hasEnded())
|
|
442
|
+
break;
|
|
443
|
+
if (!valid) {
|
|
444
|
+
this.#pending = undefined;
|
|
445
|
+
await item.acknowledgement.reject();
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (value === undefined) {
|
|
449
|
+
// The converter could not produce a value. That includes a failed
|
|
450
|
+
// lookup, so it is not safe to acknowledge: skipping here would
|
|
451
|
+
// advance the durable cursor past a message nothing has read.
|
|
452
|
+
// Report it and stop, leaving the item replayable.
|
|
453
|
+
throw new Error("The retained message could not be decoded");
|
|
454
|
+
}
|
|
455
|
+
// Do not await between the final ownership check and the app handoff.
|
|
456
|
+
this.#cursor = item.cursor;
|
|
457
|
+
if (this.#onValue) {
|
|
458
|
+
await this.#onValue(value);
|
|
459
|
+
if (this.#hasEnded())
|
|
460
|
+
break;
|
|
461
|
+
await this.#acknowledgePending();
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
return { done: false, value };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return { done: true, value: undefined };
|
|
455
468
|
}
|
|
456
|
-
|
|
457
|
-
this.#
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (!this.#hasEnded()) {
|
|
471
|
+
try {
|
|
472
|
+
// A handler that throws must not replace the real failure. The
|
|
473
|
+
// caller needs the original cause to know why the stream ended.
|
|
474
|
+
this.#options.onError?.(error);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Reported through the rethrow below.
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
await this.return();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
458
484
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (this.isDone) {
|
|
462
|
-
return Promise.resolve({ done: true, value: undefined });
|
|
485
|
+
finally {
|
|
486
|
+
this.#reading = false;
|
|
463
487
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
488
|
+
}
|
|
489
|
+
async #close(pending) {
|
|
490
|
+
const invoke = (action) => {
|
|
491
|
+
try {
|
|
492
|
+
return Promise.resolve(action());
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return Promise.reject(error instanceof Error
|
|
496
|
+
? error
|
|
497
|
+
: new Error("Message stream close failed", { cause: error }));
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
// Invoke close before any await so it fences pending reads and host handoffs.
|
|
501
|
+
const results = await Promise.allSettled([
|
|
502
|
+
invoke(() => this.#reader.close()),
|
|
503
|
+
invoke(() => pending?.reject()),
|
|
504
|
+
]);
|
|
505
|
+
try {
|
|
506
|
+
for (const result of results) {
|
|
507
|
+
if (result.status === "rejected")
|
|
508
|
+
throw result.reason;
|
|
509
|
+
}
|
|
510
|
+
return { done: true, value: undefined };
|
|
469
511
|
}
|
|
470
|
-
|
|
471
|
-
this.#
|
|
472
|
-
}
|
|
473
|
-
}
|
|
512
|
+
finally {
|
|
513
|
+
this.#options.onEnd?.();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
474
516
|
return = () => {
|
|
475
|
-
this
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
517
|
+
if (this.#done) {
|
|
518
|
+
return this.#closing ?? Promise.resolve({ done: true, value: undefined });
|
|
519
|
+
}
|
|
520
|
+
this.#done = true;
|
|
521
|
+
const pending = this.#pending;
|
|
522
|
+
this.#pending = undefined;
|
|
523
|
+
this.#closing = this.#close(pending);
|
|
524
|
+
return this.#closing;
|
|
481
525
|
};
|
|
482
|
-
end =
|
|
526
|
+
end = this.return;
|
|
527
|
+
updateScope = (groupIds) => this.#reader.updateScope(groupIds);
|
|
528
|
+
updateFilter = (conversationType, consentStates) => this.#reader.updateFilter(conversationType, consentStates);
|
|
529
|
+
catchUpSnapshot = () => this.#reader.catchUpSnapshot();
|
|
530
|
+
catchUpChanged = () => this.#reader.catchUpChanged();
|
|
483
531
|
[Symbol.asyncIterator]() {
|
|
484
532
|
return this;
|
|
485
533
|
}
|
|
486
534
|
}
|
|
487
|
-
const usableProperties = [
|
|
488
|
-
"end",
|
|
489
|
-
"isDone",
|
|
490
|
-
"next",
|
|
491
|
-
"return",
|
|
492
|
-
Symbol.asyncIterator,
|
|
493
|
-
];
|
|
494
|
-
const isUsableProperty = (prop) => {
|
|
495
|
-
return usableProperties.includes(prop);
|
|
496
|
-
};
|
|
497
|
-
/**
|
|
498
|
-
* Creates a read-only proxy for AsyncStream instances that restricts access to consumer-only methods.
|
|
499
|
-
*
|
|
500
|
-
* This proxy only exposes the following properties and methods:
|
|
501
|
-
* - `next()`: Get the next value from the stream
|
|
502
|
-
* - `end()`: Terminate the stream and stop iteration
|
|
503
|
-
* - `return()`: Same as end(), terminates the stream
|
|
504
|
-
* - `isDone`: Boolean indicating if the stream has been terminated
|
|
505
|
-
* - `Symbol.asyncIterator`: Enables `for await` loop iteration
|
|
506
|
-
*
|
|
507
|
-
* Producer methods like `push()`, `done()`, and `flush()` are hidden to prevent
|
|
508
|
-
* consumers from accidentally modifying the stream state.
|
|
509
|
-
*
|
|
510
|
-
* @param stream - The AsyncStream instance to create a proxy for
|
|
511
|
-
* @returns A read-only proxy that implements AsyncStreamProxy<T>
|
|
512
|
-
*
|
|
513
|
-
* @example
|
|
514
|
-
* ```typescript
|
|
515
|
-
* const stream = new AsyncStream<string>();
|
|
516
|
-
* const proxy = createAsyncStreamProxy(stream);
|
|
517
|
-
*
|
|
518
|
-
* stream.push("hello");
|
|
519
|
-
* stream.push("world");
|
|
520
|
-
*
|
|
521
|
-
* for await (const value of proxy) {
|
|
522
|
-
* console.log(value); // "hello", "world"
|
|
523
|
-
* }
|
|
524
|
-
* ```
|
|
525
|
-
*/
|
|
526
|
-
function createAsyncStreamProxy(stream) {
|
|
527
|
-
return new Proxy(stream, {
|
|
528
|
-
get(target, prop, receiver) {
|
|
529
|
-
if (isUsableProperty(prop)) {
|
|
530
|
-
return Reflect.get(target, prop, receiver);
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
set() {
|
|
534
|
-
return true;
|
|
535
|
-
},
|
|
536
|
-
has(_target, prop) {
|
|
537
|
-
return isUsableProperty(prop);
|
|
538
|
-
},
|
|
539
|
-
ownKeys() {
|
|
540
|
-
return usableProperties;
|
|
541
|
-
},
|
|
542
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
543
|
-
if (isUsableProperty(prop)) {
|
|
544
|
-
return {
|
|
545
|
-
enumerable: true,
|
|
546
|
-
configurable: true,
|
|
547
|
-
value: Reflect.get(target, prop),
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
return undefined;
|
|
551
|
-
},
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
535
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
*
|
|
563
|
-
* This function is not intended to be used directly.
|
|
564
|
-
*
|
|
565
|
-
* @param streamFunction - The stream function to create a stream from
|
|
566
|
-
* @param streamValueMutator - An optional function to mutate the value emitted from the stream
|
|
567
|
-
* @param options - The options for the stream
|
|
568
|
-
* @param args - Additional arguments to pass to the stream function
|
|
569
|
-
* @returns An async iterable stream proxy
|
|
570
|
-
* @throws {StreamInvalidRetryAttemptsError} if the retryAttempts option is less than 0 and retryOnFail is true
|
|
571
|
-
* @throws {StreamFailedError} if the stream fails and can't be restarted
|
|
572
|
-
*/
|
|
573
|
-
const createStream = async (streamFunction, streamValueMutator, options) => {
|
|
574
|
-
const { onError, onFail, onRestart, onRetry, onValue, retryAttempts = DEFAULT_RETRY_ATTEMPTS, retryDelay = DEFAULT_RETRY_DELAY, retryOnFail = true, } = options ?? {};
|
|
575
|
-
// retry attempts must be greater than 0
|
|
576
|
-
if (retryOnFail && retryAttempts < 0) {
|
|
577
|
-
throw new StreamInvalidRetryAttemptsError();
|
|
536
|
+
/** A notification failure with a stable error code. */
|
|
537
|
+
class NotificationError extends Error {
|
|
538
|
+
code;
|
|
539
|
+
constructor(code, options) {
|
|
540
|
+
super(code, options);
|
|
541
|
+
this.code = code;
|
|
542
|
+
this.name = "NotificationError";
|
|
578
543
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
try {
|
|
589
|
-
// if a streamValueMutator is provided, mutate the value
|
|
590
|
-
if (streamValueMutator) {
|
|
591
|
-
const mutatedValue = streamValueMutator(value);
|
|
592
|
-
if (isPromise(mutatedValue)) {
|
|
593
|
-
void mutatedValue
|
|
594
|
-
.then((mutatedValue) => {
|
|
595
|
-
if (mutatedValue !== undefined) {
|
|
596
|
-
asyncStream.push(mutatedValue);
|
|
597
|
-
onValue?.(mutatedValue);
|
|
598
|
-
}
|
|
599
|
-
})
|
|
600
|
-
.catch((error) => {
|
|
601
|
-
onError?.(error);
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
if (mutatedValue !== undefined) {
|
|
606
|
-
asyncStream.push(mutatedValue);
|
|
607
|
-
onValue?.(mutatedValue);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
asyncStream.push(value);
|
|
613
|
-
onValue?.(value);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
onError?.(error);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
544
|
+
}
|
|
545
|
+
const toBindingNotificationConfig = (config) => {
|
|
546
|
+
const { channel, ...rules } = config;
|
|
547
|
+
return {
|
|
548
|
+
...rules,
|
|
549
|
+
channel: channel.type,
|
|
550
|
+
...(channel.type === "http"
|
|
551
|
+
? { url: channel.url, signingKey: Array.from(channel.signingKey) }
|
|
552
|
+
: { token: channel.token }),
|
|
620
553
|
};
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
asyncStream.onDone = () => {
|
|
643
|
-
streamCloser.end();
|
|
554
|
+
};
|
|
555
|
+
const toNotificationState = (value) => {
|
|
556
|
+
switch (value.state) {
|
|
557
|
+
case 0 /* NotificationStateKind.Disabled */:
|
|
558
|
+
return { state: "disabled" };
|
|
559
|
+
case 1 /* NotificationStateKind.Enabled */:
|
|
560
|
+
return { state: "enabled" };
|
|
561
|
+
case 2 /* NotificationStateKind.Failed */: {
|
|
562
|
+
const codes = {
|
|
563
|
+
[0 /* NotificationFailure.PermissionDenied */]: "PermissionDenied",
|
|
564
|
+
[1 /* NotificationFailure.InvalidArgument */]: "InvalidArgument",
|
|
565
|
+
[2 /* NotificationFailure.OutOfRange */]: "OutOfRange",
|
|
566
|
+
[3 /* NotificationFailure.Unimplemented */]: "Unimplemented",
|
|
567
|
+
[4 /* NotificationFailure.ChannelNotConfigured */]: "ChannelNotConfigured",
|
|
568
|
+
};
|
|
569
|
+
if (value.failure == null) {
|
|
570
|
+
throw new Error("Notification failure is missing its error");
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
state: "failed",
|
|
574
|
+
error: new NotificationError(`NotificationError::${codes[value.failure]}`),
|
|
644
575
|
};
|
|
645
|
-
// stream restarted, call the onRestart callback
|
|
646
|
-
onRestart?.();
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
onError?.(error);
|
|
650
|
-
// retry
|
|
651
|
-
void retry(retries - 1);
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
const startRetry = () => {
|
|
655
|
-
// if the stream should be retried, start the process
|
|
656
|
-
if (retryOnFail) {
|
|
657
|
-
void retry();
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
void asyncStream.end();
|
|
661
|
-
// stream failed and should not be retried, throw an error
|
|
662
|
-
onError?.(new StreamFailedError(0));
|
|
663
576
|
}
|
|
664
|
-
};
|
|
665
|
-
try {
|
|
666
|
-
// create the stream
|
|
667
|
-
const streamCloser = await streamFunction(streamCallback, () => {
|
|
668
|
-
// call the onFail callback
|
|
669
|
-
onFail?.();
|
|
670
|
-
startRetry();
|
|
671
|
-
});
|
|
672
|
-
await streamCloser.waitForReady();
|
|
673
|
-
// when the async stream is done, end the stream
|
|
674
|
-
asyncStream.onDone = () => {
|
|
675
|
-
streamCloser.end();
|
|
676
|
-
};
|
|
677
577
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
578
|
+
};
|
|
579
|
+
const toBindingNotificationOverride = (value) => {
|
|
580
|
+
switch (value) {
|
|
581
|
+
case "enabled":
|
|
582
|
+
return 0 /* BindingNotificationOverride.Enabled */;
|
|
583
|
+
case "disabled":
|
|
584
|
+
return 1 /* BindingNotificationOverride.Disabled */;
|
|
585
|
+
case "default":
|
|
586
|
+
return 2 /* BindingNotificationOverride.Default */;
|
|
681
587
|
}
|
|
682
|
-
|
|
683
|
-
|
|
588
|
+
};
|
|
589
|
+
const throwNotificationError = (error) => {
|
|
590
|
+
if (error instanceof Error) {
|
|
591
|
+
const code = /^\[(NotificationError::[^\]]+)\]/.exec(error.message)?.[1];
|
|
592
|
+
if (code)
|
|
593
|
+
throw new NotificationError(code, { cause: error });
|
|
594
|
+
}
|
|
595
|
+
throw error;
|
|
684
596
|
};
|
|
685
597
|
|
|
686
598
|
/**
|
|
@@ -716,6 +628,16 @@ class Conversation {
|
|
|
716
628
|
get isActive() {
|
|
717
629
|
return this.#conversation.isActive();
|
|
718
630
|
}
|
|
631
|
+
/** Override notification rules, or reset the override with "default". */
|
|
632
|
+
setNotifications(value) {
|
|
633
|
+
return Promise.resolve().then(() => {
|
|
634
|
+
this.#conversation.setNotifications(toBindingNotificationOverride(value));
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/** Read the effective notification value for this conversation. */
|
|
638
|
+
notificationsEnabled() {
|
|
639
|
+
return Promise.resolve().then(() => this.#conversation.notificationsEnabled());
|
|
640
|
+
}
|
|
719
641
|
/**
|
|
720
642
|
* Gets the inbox ID that added this client's inbox to the conversation
|
|
721
643
|
*/
|
|
@@ -735,7 +657,7 @@ class Conversation {
|
|
|
735
657
|
return nsToDate(this.createdAtNs);
|
|
736
658
|
}
|
|
737
659
|
get topic() {
|
|
738
|
-
return
|
|
660
|
+
return `00${this.id}`;
|
|
739
661
|
}
|
|
740
662
|
pausedForVersion() {
|
|
741
663
|
return this.#conversation.pausedForVersion() ?? undefined;
|
|
@@ -777,26 +699,31 @@ class Conversation {
|
|
|
777
699
|
return this.#conversation.sync();
|
|
778
700
|
}
|
|
779
701
|
/**
|
|
780
|
-
*
|
|
702
|
+
* Reads retained messages after the default acknowledgement position.
|
|
703
|
+
* Set `from` to replay after a cursor without changing default progress.
|
|
704
|
+
* Set `onValue` for callback mode, or request items with the iterator.
|
|
705
|
+
* Core owns network recovery. Legacy retry options and `disableSync` do not apply.
|
|
781
706
|
*
|
|
782
|
-
* @param options - Optional
|
|
707
|
+
* @param options - Optional delivery callbacks and replay cursor
|
|
783
708
|
* @returns Stream instance for new messages
|
|
784
709
|
*/
|
|
785
710
|
async stream(options) {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
await this.sync();
|
|
789
|
-
}
|
|
790
|
-
return this.#conversation.stream(callback, onFail);
|
|
791
|
-
};
|
|
792
|
-
const convertMessage = (value) => {
|
|
711
|
+
const reader = await this.#conversation.messageReader(options?.from);
|
|
712
|
+
const convertMessage = (value, cursor) => {
|
|
793
713
|
const enrichedMessage = this.#client.conversations.getMessageById(value.id);
|
|
794
|
-
if (enrichedMessage
|
|
795
|
-
|
|
714
|
+
if (enrichedMessage !== undefined) {
|
|
715
|
+
assertMessageDecodedForDelivery(enrichedMessage);
|
|
716
|
+
enrichedMessage.deliveryCursor = cursor;
|
|
796
717
|
}
|
|
797
718
|
return enrichedMessage;
|
|
798
719
|
};
|
|
799
|
-
return
|
|
720
|
+
return new MessageStream(reader, convertMessage, options);
|
|
721
|
+
}
|
|
722
|
+
messageHistorySnapshot(limit) {
|
|
723
|
+
return this.#conversation.messageHistorySnapshot(limit);
|
|
724
|
+
}
|
|
725
|
+
beginningDeliveryCursor() {
|
|
726
|
+
return this.#conversation.beginningDeliveryCursor();
|
|
800
727
|
}
|
|
801
728
|
/**
|
|
802
729
|
* Decodes, decrypts, and persists a raw envelope from a group message stream.
|
|
@@ -1164,9 +1091,19 @@ class Group extends Conversation {
|
|
|
1164
1091
|
* Updates the group's app data (max 8192 bytes)
|
|
1165
1092
|
*
|
|
1166
1093
|
* @param appData The new app data for the group
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1094
|
+
* @param expectedAppData Optional compare-and-swap guard. When provided, the
|
|
1095
|
+
* update is abandoned — rather than overwriting — if the committed value is
|
|
1096
|
+
* no longer this, including when another member's commit wins the race after
|
|
1097
|
+
* this update was published. Callers reconciling structured state should
|
|
1098
|
+
* pass the value they merged against, so a concurrent write surfaces as an
|
|
1099
|
+
* error instead of silently discarding someone else's change. Omit for
|
|
1100
|
+
* last-writer-wins.
|
|
1101
|
+
*/
|
|
1102
|
+
async updateAppData(appData, expectedAppData) {
|
|
1103
|
+
return this.#conversation.updateAppData({
|
|
1104
|
+
value: appData,
|
|
1105
|
+
expectedValue: expectedAppData,
|
|
1106
|
+
});
|
|
1170
1107
|
}
|
|
1171
1108
|
/**
|
|
1172
1109
|
* The permissions of the group
|
|
@@ -1288,16 +1225,420 @@ class Group extends Conversation {
|
|
|
1288
1225
|
async requestRemoval() {
|
|
1289
1226
|
return this.#conversation.leaveGroup();
|
|
1290
1227
|
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Checks if the current user has requested to leave the group
|
|
1293
|
-
*
|
|
1294
|
-
* @returns Boolean
|
|
1295
|
-
*/
|
|
1296
|
-
isPendingRemoval() {
|
|
1297
|
-
return (this.#conversation.membershipState() ===
|
|
1298
|
-
4 /* GroupMembershipState.PendingRemove */);
|
|
1228
|
+
/**
|
|
1229
|
+
* Checks if the current user has requested to leave the group
|
|
1230
|
+
*
|
|
1231
|
+
* @returns Boolean
|
|
1232
|
+
*/
|
|
1233
|
+
isPendingRemoval() {
|
|
1234
|
+
return (this.#conversation.membershipState() ===
|
|
1235
|
+
4 /* GroupMembershipState.PendingRemove */);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* AsyncStream provides an async iterable interface for streaming data.
|
|
1241
|
+
*
|
|
1242
|
+
* This class implements a producer-consumer pattern where:
|
|
1243
|
+
* - Producers can push values using the `push()` method
|
|
1244
|
+
* - Consumers can iterate over values asynchronously using `for await` loops or `next()`
|
|
1245
|
+
* - Values are queued internally when no consumers are waiting
|
|
1246
|
+
* - Consumers are resolved immediately when values are available
|
|
1247
|
+
* - The stream can be terminated using `done()`, `return()`, or `end()`
|
|
1248
|
+
*
|
|
1249
|
+
* @example
|
|
1250
|
+
* ```typescript
|
|
1251
|
+
* const stream = new AsyncStream<string>();
|
|
1252
|
+
*
|
|
1253
|
+
* stream.push("hello");
|
|
1254
|
+
* stream.push("world");
|
|
1255
|
+
*
|
|
1256
|
+
* for await (const value of stream) {
|
|
1257
|
+
* console.log(value); // "hello", "world"
|
|
1258
|
+
* }
|
|
1259
|
+
* ```
|
|
1260
|
+
*/
|
|
1261
|
+
class AsyncStream {
|
|
1262
|
+
isDone = false;
|
|
1263
|
+
#pendingResolves = [];
|
|
1264
|
+
#queue;
|
|
1265
|
+
onDone;
|
|
1266
|
+
onReturn;
|
|
1267
|
+
constructor() {
|
|
1268
|
+
this.#queue = [];
|
|
1269
|
+
this.isDone = false;
|
|
1270
|
+
}
|
|
1271
|
+
flush() {
|
|
1272
|
+
while (this.#pendingResolves.length > 0) {
|
|
1273
|
+
const nextResolve = this.#pendingResolves.shift();
|
|
1274
|
+
if (nextResolve) {
|
|
1275
|
+
nextResolve({ done: true, value: undefined });
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
done() {
|
|
1280
|
+
this.flush();
|
|
1281
|
+
this.#queue = [];
|
|
1282
|
+
this.#pendingResolves = [];
|
|
1283
|
+
this.isDone = true;
|
|
1284
|
+
this.onDone?.();
|
|
1285
|
+
}
|
|
1286
|
+
push = (value) => {
|
|
1287
|
+
if (this.isDone) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const nextResolve = this.#pendingResolves.shift();
|
|
1291
|
+
if (nextResolve) {
|
|
1292
|
+
nextResolve({
|
|
1293
|
+
done: false,
|
|
1294
|
+
value,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
this.#queue.push(value);
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
next = () => {
|
|
1302
|
+
if (this.isDone) {
|
|
1303
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
1304
|
+
}
|
|
1305
|
+
if (this.#queue.length > 0) {
|
|
1306
|
+
return Promise.resolve({
|
|
1307
|
+
done: false,
|
|
1308
|
+
value: this.#queue.shift(),
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
return new Promise((resolve) => {
|
|
1312
|
+
this.#pendingResolves.push(resolve);
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1315
|
+
return = () => {
|
|
1316
|
+
this.onReturn?.();
|
|
1317
|
+
this.done();
|
|
1318
|
+
return Promise.resolve({
|
|
1319
|
+
done: true,
|
|
1320
|
+
value: undefined,
|
|
1321
|
+
});
|
|
1322
|
+
};
|
|
1323
|
+
end = () => this.return();
|
|
1324
|
+
[Symbol.asyncIterator]() {
|
|
1325
|
+
return this;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const usableProperties = [
|
|
1329
|
+
"end",
|
|
1330
|
+
"isDone",
|
|
1331
|
+
"next",
|
|
1332
|
+
"return",
|
|
1333
|
+
Symbol.asyncIterator,
|
|
1334
|
+
];
|
|
1335
|
+
const isUsableProperty = (prop) => {
|
|
1336
|
+
return usableProperties.includes(prop);
|
|
1337
|
+
};
|
|
1338
|
+
/**
|
|
1339
|
+
* Creates a read-only proxy for AsyncStream instances that restricts access to consumer-only methods.
|
|
1340
|
+
*
|
|
1341
|
+
* This proxy only exposes the following properties and methods:
|
|
1342
|
+
* - `next()`: Get the next value from the stream
|
|
1343
|
+
* - `end()`: Terminate the stream and stop iteration
|
|
1344
|
+
* - `return()`: Same as end(), terminates the stream
|
|
1345
|
+
* - `isDone`: Boolean indicating if the stream has been terminated
|
|
1346
|
+
* - `Symbol.asyncIterator`: Enables `for await` loop iteration
|
|
1347
|
+
*
|
|
1348
|
+
* Producer methods like `push()`, `done()`, and `flush()` are hidden to prevent
|
|
1349
|
+
* consumers from accidentally modifying the stream state.
|
|
1350
|
+
*
|
|
1351
|
+
* @param stream - The AsyncStream instance to create a proxy for
|
|
1352
|
+
* @returns A read-only proxy that implements AsyncStreamProxy<T>
|
|
1353
|
+
*
|
|
1354
|
+
* @example
|
|
1355
|
+
* ```typescript
|
|
1356
|
+
* const stream = new AsyncStream<string>();
|
|
1357
|
+
* const proxy = createAsyncStreamProxy(stream);
|
|
1358
|
+
*
|
|
1359
|
+
* stream.push("hello");
|
|
1360
|
+
* stream.push("world");
|
|
1361
|
+
*
|
|
1362
|
+
* for await (const value of proxy) {
|
|
1363
|
+
* console.log(value); // "hello", "world"
|
|
1364
|
+
* }
|
|
1365
|
+
* ```
|
|
1366
|
+
*/
|
|
1367
|
+
function createAsyncStreamProxy(stream) {
|
|
1368
|
+
return new Proxy(stream, {
|
|
1369
|
+
get(target, prop, receiver) {
|
|
1370
|
+
if (isUsableProperty(prop)) {
|
|
1371
|
+
return Reflect.get(target, prop, receiver);
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
set() {
|
|
1375
|
+
return true;
|
|
1376
|
+
},
|
|
1377
|
+
has(_target, prop) {
|
|
1378
|
+
return isUsableProperty(prop);
|
|
1379
|
+
},
|
|
1380
|
+
ownKeys() {
|
|
1381
|
+
return usableProperties;
|
|
1382
|
+
},
|
|
1383
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
1384
|
+
if (isUsableProperty(prop)) {
|
|
1385
|
+
return {
|
|
1386
|
+
enumerable: true,
|
|
1387
|
+
configurable: true,
|
|
1388
|
+
value: Reflect.get(target, prop),
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
return undefined;
|
|
1392
|
+
},
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const DEFAULT_RETRY_DELAY = 60_000; // milliseconds
|
|
1397
|
+
const DEFAULT_RETRY_ATTEMPTS = 10;
|
|
1398
|
+
/**
|
|
1399
|
+
* Creates a stream from a stream function
|
|
1400
|
+
*
|
|
1401
|
+
* If the stream fails, an attempt will be made to restart it.
|
|
1402
|
+
*
|
|
1403
|
+
* Ending the stream is terminal: no callbacks are invoked and no native
|
|
1404
|
+
* stream is created after the stream ends.
|
|
1405
|
+
*
|
|
1406
|
+
* This function is not intended to be used directly.
|
|
1407
|
+
*
|
|
1408
|
+
* @param streamFunction - The stream function to create a stream from
|
|
1409
|
+
* @param streamValueMutator - An optional function to mutate the value emitted from the stream
|
|
1410
|
+
* @param options - The options for the stream
|
|
1411
|
+
* @returns An async iterable stream proxy
|
|
1412
|
+
* @throws {StreamInvalidRetryAttemptsError} if the retryAttempts option is less than 0 and retryOnFail is true
|
|
1413
|
+
* @throws {StreamFailedError} if the stream fails and can't be restarted
|
|
1414
|
+
*/
|
|
1415
|
+
const createStream = async (streamFunction, streamValueMutator, options) => {
|
|
1416
|
+
const { onEnd, onError, onFail, onRestart, onRetry, onValue, retryAttempts = DEFAULT_RETRY_ATTEMPTS, retryDelay = DEFAULT_RETRY_DELAY, retryOnFail = true, } = options ?? {};
|
|
1417
|
+
// retry attempts must be greater than 0
|
|
1418
|
+
if (retryOnFail && retryAttempts < 0) {
|
|
1419
|
+
throw new StreamInvalidRetryAttemptsError();
|
|
1420
|
+
}
|
|
1421
|
+
const asyncStream = new AsyncStream();
|
|
1422
|
+
// lifecycle state, owned by this wrapper
|
|
1423
|
+
let stopped = false;
|
|
1424
|
+
// reading the flag through a function call defeats TS control-flow
|
|
1425
|
+
// narrowing, which cannot see the closure mutation across awaits
|
|
1426
|
+
const isStopped = () => stopped;
|
|
1427
|
+
let currentCloser;
|
|
1428
|
+
let retryTimer;
|
|
1429
|
+
let retryInFlight = false;
|
|
1430
|
+
// set when a restart's native stream closes during its own creation, so the
|
|
1431
|
+
// completed attempt reschedules instead of installing an already-dead closer
|
|
1432
|
+
let closePendingDuringRestart = false;
|
|
1433
|
+
// read through a call so no-unnecessary-condition cannot narrow the flag to
|
|
1434
|
+
// a constant; handleNativeClose mutates it across an await
|
|
1435
|
+
const isClosePending = () => closePendingDuringRestart;
|
|
1436
|
+
// the retry budget is monotonic: it is never reset for the lifetime of
|
|
1437
|
+
// this wrapper, so restarts are bounded even across successful restarts
|
|
1438
|
+
let remainingRetries = retryAttempts;
|
|
1439
|
+
// terminal transition: cancel any pending retry, close the active native
|
|
1440
|
+
// stream, and notify onEnd exactly once
|
|
1441
|
+
const stop = () => {
|
|
1442
|
+
if (isStopped()) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
stopped = true;
|
|
1446
|
+
closePendingDuringRestart = false;
|
|
1447
|
+
if (retryTimer !== undefined) {
|
|
1448
|
+
clearTimeout(retryTimer);
|
|
1449
|
+
retryTimer = undefined;
|
|
1450
|
+
}
|
|
1451
|
+
currentCloser?.end();
|
|
1452
|
+
currentCloser = undefined;
|
|
1453
|
+
onEnd?.();
|
|
1454
|
+
};
|
|
1455
|
+
// registered before any async work so ending the stream is always terminal,
|
|
1456
|
+
// even while a retry is pending or a native stream is being created
|
|
1457
|
+
asyncStream.onDone = stop;
|
|
1458
|
+
const fail = (error) => {
|
|
1459
|
+
if (isStopped()) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
// the terminal transition must run even if onError throws, otherwise a
|
|
1463
|
+
// throwing consumer callback leaves the stream open and hangs next()
|
|
1464
|
+
try {
|
|
1465
|
+
onError?.(error);
|
|
1466
|
+
}
|
|
1467
|
+
finally {
|
|
1468
|
+
void asyncStream.end();
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const handleAsyncError = (error) => {
|
|
1472
|
+
if (!isStopped()) {
|
|
1473
|
+
onError?.(error);
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
const streamCallback = (error, value) => {
|
|
1477
|
+
// an ended stream must not invoke any callbacks
|
|
1478
|
+
if (isStopped()) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
// if a stream error occurs, call the onError callback
|
|
1482
|
+
if (error) {
|
|
1483
|
+
onError?.(error);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
// ensure the value is not undefined
|
|
1487
|
+
if (value !== undefined) {
|
|
1488
|
+
try {
|
|
1489
|
+
// if a streamValueMutator is provided, mutate the value
|
|
1490
|
+
if (streamValueMutator) {
|
|
1491
|
+
const mutatedValue = streamValueMutator(value);
|
|
1492
|
+
if (isPromise(mutatedValue)) {
|
|
1493
|
+
void mutatedValue
|
|
1494
|
+
.then((mutatedValue) => {
|
|
1495
|
+
// the stream may have ended while the value was mutating
|
|
1496
|
+
if (!isStopped() && mutatedValue !== undefined) {
|
|
1497
|
+
asyncStream.push(mutatedValue);
|
|
1498
|
+
return onValue?.(mutatedValue);
|
|
1499
|
+
}
|
|
1500
|
+
})
|
|
1501
|
+
.catch(handleAsyncError);
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
// a synchronous mutator may have ended the stream; gate delivery
|
|
1505
|
+
// on the stopped flag to match the async branch above
|
|
1506
|
+
if (!isStopped() && mutatedValue !== undefined) {
|
|
1507
|
+
asyncStream.push(mutatedValue);
|
|
1508
|
+
void Promise.resolve(onValue?.(mutatedValue)).catch(handleAsyncError);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
else {
|
|
1513
|
+
asyncStream.push(value);
|
|
1514
|
+
void Promise.resolve(onValue?.(value)).catch(handleAsyncError);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
onError?.(error);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
const scheduleRetry = () => {
|
|
1523
|
+
// at most one retry may be in flight per wrapper
|
|
1524
|
+
if (isStopped() || retryInFlight) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (remainingRetries <= 0) {
|
|
1528
|
+
fail(new StreamFailedError(retryAttempts));
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
retryInFlight = true;
|
|
1532
|
+
retryTimer = setTimeout(() => {
|
|
1533
|
+
retryTimer = undefined;
|
|
1534
|
+
void attemptRestart();
|
|
1535
|
+
}, retryDelay);
|
|
1536
|
+
};
|
|
1537
|
+
const attemptRestart = async () => {
|
|
1538
|
+
if (isStopped()) {
|
|
1539
|
+
retryInFlight = false;
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
// scope the pending-close flag to this attempt: a close recorded while the
|
|
1543
|
+
// retry timer was merely pending belongs to the stream that scheduled it
|
|
1544
|
+
closePendingDuringRestart = false;
|
|
1545
|
+
remainingRetries -= 1;
|
|
1546
|
+
onRetry?.(retryAttempts - remainingRetries, retryAttempts);
|
|
1547
|
+
if (isStopped()) {
|
|
1548
|
+
// onRetry may have ended the stream; do not open a native stream after
|
|
1549
|
+
// termination
|
|
1550
|
+
retryInFlight = false;
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
// attempt to restart the stream
|
|
1555
|
+
const streamCloser = await streamFunction(streamCallback, handleNativeClose);
|
|
1556
|
+
if (isStopped()) {
|
|
1557
|
+
// the stream ended while the native stream was being created
|
|
1558
|
+
streamCloser.end();
|
|
1559
|
+
retryInFlight = false;
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
await streamCloser.waitForReady();
|
|
1563
|
+
if (isStopped()) {
|
|
1564
|
+
streamCloser.end();
|
|
1565
|
+
retryInFlight = false;
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (isClosePending()) {
|
|
1569
|
+
// the replacement stream closed during its own creation; discard it
|
|
1570
|
+
// and schedule a fresh attempt instead of installing a dead closer
|
|
1571
|
+
closePendingDuringRestart = false;
|
|
1572
|
+
streamCloser.end();
|
|
1573
|
+
retryInFlight = false;
|
|
1574
|
+
scheduleRetry();
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
currentCloser = streamCloser;
|
|
1578
|
+
retryInFlight = false;
|
|
1579
|
+
// stream restarted, call the onRestart callback
|
|
1580
|
+
onRestart?.();
|
|
1581
|
+
}
|
|
1582
|
+
catch (error) {
|
|
1583
|
+
retryInFlight = false;
|
|
1584
|
+
if (isStopped()) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
closePendingDuringRestart = false;
|
|
1588
|
+
onError?.(error);
|
|
1589
|
+
scheduleRetry();
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const handleNativeClose = () => {
|
|
1593
|
+
// ending the stream closes the native stream, which still triggers this
|
|
1594
|
+
// callback; only an unexpected close is a failure
|
|
1595
|
+
if (isStopped()) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
currentCloser = undefined;
|
|
1599
|
+
onFail?.();
|
|
1600
|
+
if (retryOnFail) {
|
|
1601
|
+
// a native close during an in-flight restart is dropped by the
|
|
1602
|
+
// single-flight guard; record it so the completed attempt reschedules
|
|
1603
|
+
// instead of installing a stream that already died
|
|
1604
|
+
if (retryInFlight) {
|
|
1605
|
+
closePendingDuringRestart = true;
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
scheduleRetry();
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
fail(new StreamFailedError(0));
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
try {
|
|
1615
|
+
// create the stream
|
|
1616
|
+
const streamCloser = await streamFunction(streamCallback, handleNativeClose);
|
|
1617
|
+
if (isStopped()) {
|
|
1618
|
+
streamCloser.end();
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
await streamCloser.waitForReady();
|
|
1622
|
+
if (isStopped()) {
|
|
1623
|
+
streamCloser.end();
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
currentCloser = streamCloser;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
catch (error) {
|
|
1631
|
+
onError?.(error);
|
|
1632
|
+
if (retryOnFail) {
|
|
1633
|
+
scheduleRetry();
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
fail(new StreamFailedError(0));
|
|
1637
|
+
}
|
|
1299
1638
|
}
|
|
1300
|
-
|
|
1639
|
+
// return a proxy for the async stream
|
|
1640
|
+
return createAsyncStreamProxy(asyncStream);
|
|
1641
|
+
};
|
|
1301
1642
|
|
|
1302
1643
|
/**
|
|
1303
1644
|
* Manages conversations
|
|
@@ -1321,14 +1662,14 @@ class Conversations {
|
|
|
1321
1662
|
this.#conversations = conversations;
|
|
1322
1663
|
}
|
|
1323
1664
|
get topic() {
|
|
1324
|
-
return
|
|
1665
|
+
return `01${this.#client.installationId}`;
|
|
1325
1666
|
}
|
|
1326
1667
|
/**
|
|
1327
1668
|
* Retrieves a conversation by its ID
|
|
1328
1669
|
*
|
|
1329
1670
|
* @param id - The conversation ID to look up
|
|
1330
1671
|
* @returns The conversation if found, undefined otherwise
|
|
1331
|
-
* @see https://docs.xmtp.org/
|
|
1672
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1332
1673
|
*/
|
|
1333
1674
|
async getConversationById(id) {
|
|
1334
1675
|
try {
|
|
@@ -1353,7 +1694,7 @@ class Conversations {
|
|
|
1353
1694
|
*
|
|
1354
1695
|
* @param inboxId - The inbox ID to look up
|
|
1355
1696
|
* @returns The DM if found, undefined otherwise
|
|
1356
|
-
* @see https://docs.xmtp.org/
|
|
1697
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1357
1698
|
*/
|
|
1358
1699
|
getDmByInboxId(inboxId) {
|
|
1359
1700
|
try {
|
|
@@ -1370,7 +1711,7 @@ class Conversations {
|
|
|
1370
1711
|
*
|
|
1371
1712
|
* @param identifier - The identifier to look up
|
|
1372
1713
|
* @returns Promise that resolves with the DM, if found
|
|
1373
|
-
* @see https://docs.xmtp.org/
|
|
1714
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1374
1715
|
*/
|
|
1375
1716
|
async fetchDmByIdentifier(identifier) {
|
|
1376
1717
|
const inboxId = await this.#client.fetchInboxIdByIdentifier(identifier);
|
|
@@ -1384,7 +1725,7 @@ class Conversations {
|
|
|
1384
1725
|
*
|
|
1385
1726
|
* @param id - The message ID to look up
|
|
1386
1727
|
* @returns The decoded message if found, undefined otherwise
|
|
1387
|
-
* @see https://docs.xmtp.org/
|
|
1728
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1388
1729
|
*/
|
|
1389
1730
|
getMessageById(id) {
|
|
1390
1731
|
try {
|
|
@@ -1401,7 +1742,7 @@ class Conversations {
|
|
|
1401
1742
|
*
|
|
1402
1743
|
* @param options - Optional group creation options
|
|
1403
1744
|
* @returns The new group
|
|
1404
|
-
* @see https://docs.xmtp.org/
|
|
1745
|
+
* @see https://docs.xmtp.org/sdk/conversations/#optimistically-create-a-group
|
|
1405
1746
|
*/
|
|
1406
1747
|
createGroupOptimistic(options) {
|
|
1407
1748
|
const group = this.#conversations.createGroupOptimistic(options);
|
|
@@ -1413,7 +1754,7 @@ class Conversations {
|
|
|
1413
1754
|
* @param identifiers - Array of identifiers for group members
|
|
1414
1755
|
* @param options - Optional group creation options
|
|
1415
1756
|
* @returns The new group
|
|
1416
|
-
* @see https://docs.xmtp.org/
|
|
1757
|
+
* @see https://docs.xmtp.org/sdk/conversations/#create-a-group
|
|
1417
1758
|
*/
|
|
1418
1759
|
async createGroupWithIdentifiers(identifiers, options) {
|
|
1419
1760
|
const group = await this.#conversations.createGroupByIdentity(identifiers, options);
|
|
@@ -1426,7 +1767,7 @@ class Conversations {
|
|
|
1426
1767
|
* @param inboxIds - Array of inbox IDs for group members
|
|
1427
1768
|
* @param options - Optional group creation options
|
|
1428
1769
|
* @returns The new group
|
|
1429
|
-
* @see https://docs.xmtp.org/
|
|
1770
|
+
* @see https://docs.xmtp.org/sdk/conversations/#create-a-group
|
|
1430
1771
|
*/
|
|
1431
1772
|
async createGroup(inboxIds, options) {
|
|
1432
1773
|
const group = await this.#conversations.createGroup(inboxIds, options);
|
|
@@ -1439,7 +1780,7 @@ class Conversations {
|
|
|
1439
1780
|
* @param identifier - Identifier for the DM recipient
|
|
1440
1781
|
* @param options - Optional DM creation options
|
|
1441
1782
|
* @returns The new DM
|
|
1442
|
-
* @see https://docs.xmtp.org/
|
|
1783
|
+
* @see https://docs.xmtp.org/sdk/conversations/#create-a-dm
|
|
1443
1784
|
*/
|
|
1444
1785
|
async createDmWithIdentifier(identifier, options) {
|
|
1445
1786
|
const group = await this.#conversations.createDmByIdentity(identifier, options);
|
|
@@ -1452,7 +1793,7 @@ class Conversations {
|
|
|
1452
1793
|
* @param inboxId - Inbox ID for the DM recipient
|
|
1453
1794
|
* @param options - Optional DM creation options
|
|
1454
1795
|
* @returns The new DM
|
|
1455
|
-
* @see https://docs.xmtp.org/
|
|
1796
|
+
* @see https://docs.xmtp.org/sdk/conversations/#create-a-dm
|
|
1456
1797
|
*/
|
|
1457
1798
|
async createDm(inboxId, options) {
|
|
1458
1799
|
const group = await this.#conversations.createDm(inboxId, options);
|
|
@@ -1464,7 +1805,7 @@ class Conversations {
|
|
|
1464
1805
|
*
|
|
1465
1806
|
* @param options - Optional filtering and pagination options
|
|
1466
1807
|
* @returns Array of conversations
|
|
1467
|
-
* @see https://docs.xmtp.org/
|
|
1808
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1468
1809
|
*/
|
|
1469
1810
|
async list(options) {
|
|
1470
1811
|
const groups = this.#conversations.list(options);
|
|
@@ -1487,7 +1828,7 @@ class Conversations {
|
|
|
1487
1828
|
*
|
|
1488
1829
|
* @param options - Optional filtering and pagination options
|
|
1489
1830
|
* @returns Array of groups
|
|
1490
|
-
* @see https://docs.xmtp.org/
|
|
1831
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1491
1832
|
*/
|
|
1492
1833
|
listGroups(options) {
|
|
1493
1834
|
const groups = this.#conversations.list({
|
|
@@ -1504,7 +1845,7 @@ class Conversations {
|
|
|
1504
1845
|
*
|
|
1505
1846
|
* @param options - Optional filtering and pagination options
|
|
1506
1847
|
* @returns Array of DMs
|
|
1507
|
-
* @see https://docs.xmtp.org/
|
|
1848
|
+
* @see https://docs.xmtp.org/sdk/conversations/
|
|
1508
1849
|
*/
|
|
1509
1850
|
listDms(options) {
|
|
1510
1851
|
const groups = this.#conversations.list({
|
|
@@ -1520,7 +1861,7 @@ class Conversations {
|
|
|
1520
1861
|
* Synchronizes conversations for the current client from the network
|
|
1521
1862
|
*
|
|
1522
1863
|
* @returns Promise that resolves when sync is complete
|
|
1523
|
-
* @see https://docs.xmtp.org/
|
|
1864
|
+
* @see https://docs.xmtp.org/sdk/sync/
|
|
1524
1865
|
*/
|
|
1525
1866
|
async sync() {
|
|
1526
1867
|
return this.#conversations.sync();
|
|
@@ -1531,7 +1872,7 @@ class Conversations {
|
|
|
1531
1872
|
*
|
|
1532
1873
|
* @param consentStates - Optional array of consent states to filter by
|
|
1533
1874
|
* @returns Promise that resolves when sync is complete
|
|
1534
|
-
* @see https://docs.xmtp.org/
|
|
1875
|
+
* @see https://docs.xmtp.org/sdk/sync/
|
|
1535
1876
|
*/
|
|
1536
1877
|
async syncAll(consentStates) {
|
|
1537
1878
|
return this.#conversations.syncAll(consentStates);
|
|
@@ -1542,7 +1883,7 @@ class Conversations {
|
|
|
1542
1883
|
* @param options - Optional stream options
|
|
1543
1884
|
* @param options.conversationType - Optional conversation type to filter by
|
|
1544
1885
|
* @returns Stream instance for new conversations
|
|
1545
|
-
* @see https://docs.xmtp.org/
|
|
1886
|
+
* @see https://docs.xmtp.org/sdk/stream/#stream-methods
|
|
1546
1887
|
*/
|
|
1547
1888
|
async stream(options) {
|
|
1548
1889
|
const stream = async (callback, onFail) => {
|
|
@@ -1571,7 +1912,7 @@ class Conversations {
|
|
|
1571
1912
|
*
|
|
1572
1913
|
* @param options - Optional stream options
|
|
1573
1914
|
* @returns Stream instance for new group conversations
|
|
1574
|
-
* @see https://docs.xmtp.org/
|
|
1915
|
+
* @see https://docs.xmtp.org/sdk/stream/#stream-methods
|
|
1575
1916
|
*/
|
|
1576
1917
|
async streamGroups(options) {
|
|
1577
1918
|
const stream = async (callback, onFail) => {
|
|
@@ -1590,7 +1931,7 @@ class Conversations {
|
|
|
1590
1931
|
*
|
|
1591
1932
|
* @param options - Optional stream options
|
|
1592
1933
|
* @returns Stream instance for new DM conversations
|
|
1593
|
-
* @see https://docs.xmtp.org/
|
|
1934
|
+
* @see https://docs.xmtp.org/sdk/stream/#stream-methods
|
|
1594
1935
|
*/
|
|
1595
1936
|
async streamDms(options) {
|
|
1596
1937
|
const stream = async (callback, onFail) => {
|
|
@@ -1605,37 +1946,42 @@ class Conversations {
|
|
|
1605
1946
|
return createStream(stream, convertConversation, options);
|
|
1606
1947
|
}
|
|
1607
1948
|
/**
|
|
1608
|
-
*
|
|
1949
|
+
* Reads retained messages after each group's default acknowledgement position.
|
|
1950
|
+
* Set `from` to replay after a cursor without changing default progress.
|
|
1951
|
+
* Set `onValue` for callback mode, or request items with the iterator.
|
|
1952
|
+
* Core owns network recovery. Legacy retry options and `disableSync` do not apply.
|
|
1609
1953
|
*
|
|
1610
1954
|
* @param options - Optional stream options
|
|
1611
1955
|
* @param options.conversationType - Optional conversation type to filter by
|
|
1612
1956
|
* @param options.consentStates - Optional array of consent states to filter by
|
|
1613
1957
|
* @returns Stream instance for new messages
|
|
1614
|
-
* @see https://docs.xmtp.org/
|
|
1958
|
+
* @see https://docs.xmtp.org/sdk/stream/#stream-methods
|
|
1615
1959
|
*/
|
|
1616
1960
|
async streamAllMessages(options) {
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
await this.syncAll(options?.consentStates);
|
|
1620
|
-
}
|
|
1621
|
-
return this.#conversations.streamAllMessages(callback, onFail, options?.conversationType, options?.consentStates);
|
|
1622
|
-
};
|
|
1623
|
-
const convertMessage = (value) => {
|
|
1961
|
+
const reader = await this.#conversations.messageReader(options?.groupIds, options?.conversationType, options?.consentStates, options?.from);
|
|
1962
|
+
const convertMessage = (value, cursor) => {
|
|
1624
1963
|
const enrichedMessage = this.getMessageById(value.id);
|
|
1625
|
-
if (enrichedMessage
|
|
1626
|
-
|
|
1964
|
+
if (enrichedMessage !== undefined) {
|
|
1965
|
+
assertMessageDecodedForDelivery(enrichedMessage);
|
|
1966
|
+
enrichedMessage.deliveryCursor = cursor;
|
|
1627
1967
|
}
|
|
1628
1968
|
return enrichedMessage;
|
|
1629
1969
|
};
|
|
1630
|
-
return
|
|
1970
|
+
return new MessageStream(reader, convertMessage, options);
|
|
1971
|
+
}
|
|
1972
|
+
messageHistorySnapshot(limit, options) {
|
|
1973
|
+
return this.#conversations.messageHistorySnapshot(limit, options?.groupIds, options?.conversationType, options?.consentStates);
|
|
1974
|
+
}
|
|
1975
|
+
beginningDeliveryCursor() {
|
|
1976
|
+
return this.#conversations.beginningDeliveryCursor();
|
|
1631
1977
|
}
|
|
1632
1978
|
/**
|
|
1633
|
-
*
|
|
1979
|
+
* Reads retained group messages with default progress or an explicit replay cursor.
|
|
1634
1980
|
*
|
|
1635
1981
|
* @param options - Optional stream options
|
|
1636
1982
|
* @param options.consentStates - Optional array of consent states to filter by
|
|
1637
1983
|
* @returns Stream instance for new group messages
|
|
1638
|
-
* @see https://docs.xmtp.org/
|
|
1984
|
+
* @see https://docs.xmtp.org/sdk/stream/#stream-methods
|
|
1639
1985
|
*/
|
|
1640
1986
|
async streamAllGroupMessages(options) {
|
|
1641
1987
|
return this.streamAllMessages({
|
|
@@ -1645,12 +1991,12 @@ class Conversations {
|
|
|
1645
1991
|
});
|
|
1646
1992
|
}
|
|
1647
1993
|
/**
|
|
1648
|
-
*
|
|
1994
|
+
* Reads retained DM messages with default progress or an explicit replay cursor.
|
|
1649
1995
|
*
|
|
1650
1996
|
* @param options - Optional stream options
|
|
1651
1997
|
* @param options.consentStates - Optional array of consent states to filter by
|
|
1652
1998
|
* @returns Stream instance for new DM messages
|
|
1653
|
-
* @see https://docs.xmtp.org/
|
|
1999
|
+
* @see https://docs.xmtp.org/sdk/stream/#stream-methods
|
|
1654
2000
|
*/
|
|
1655
2001
|
async streamAllDmMessages(options) {
|
|
1656
2002
|
return this.streamAllMessages({
|
|
@@ -1699,7 +2045,7 @@ class Conversations {
|
|
|
1699
2045
|
* Gets the HMAC keys for all conversations
|
|
1700
2046
|
*
|
|
1701
2047
|
* @returns The HMAC keys for all conversations
|
|
1702
|
-
* @see https://docs.xmtp.org/
|
|
2048
|
+
* @see https://docs.xmtp.org/sdk/push-notifications/
|
|
1703
2049
|
*/
|
|
1704
2050
|
hmacKeys() {
|
|
1705
2051
|
return this.#conversations.hmacKeys();
|
|
@@ -1837,18 +2183,145 @@ class Preferences {
|
|
|
1837
2183
|
}
|
|
1838
2184
|
}
|
|
1839
2185
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
]
|
|
2186
|
+
/**
|
|
2187
|
+
* Typed failures for the deployment configuration a backend publishes
|
|
2188
|
+
* (spec 006 §7, CFG-083).
|
|
2189
|
+
*
|
|
2190
|
+
* The bindings report every one of these as an `Error` whose message begins
|
|
2191
|
+
* with `[ClientError::<Variant>]`. Matching on that string is what this module
|
|
2192
|
+
* removes: each of the six documented codes becomes its own class, so an app
|
|
2193
|
+
* writes `error instanceof BackendMismatchError` instead of a regular
|
|
2194
|
+
* expression over a message that may be reworded.
|
|
2195
|
+
*
|
|
2196
|
+
* Every class keeps the binding's message and the original error as `cause`,
|
|
2197
|
+
* so the identifiers, versions, scopes, or chain the core reported are still
|
|
2198
|
+
* readable for diagnostics.
|
|
2199
|
+
*/
|
|
2200
|
+
/**
|
|
2201
|
+
* Base class of the six configuration failures. Catch this to handle any of
|
|
2202
|
+
* them; catch a subclass to handle one.
|
|
2203
|
+
*/
|
|
2204
|
+
class ServerConfigurationError extends Error {
|
|
2205
|
+
code;
|
|
2206
|
+
constructor(
|
|
2207
|
+
/** The stable code the bindings reported, e.g. `ClientError::AuthRequired`. */
|
|
2208
|
+
code, message, options) {
|
|
2209
|
+
super(message, options);
|
|
2210
|
+
this.code = code;
|
|
2211
|
+
this.name = "ServerConfigurationError";
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* The configuration could not be read: the deployment did not answer, answered
|
|
2216
|
+
* `UNIMPLEMENTED` because it predates spec 006, or the copy could not be
|
|
2217
|
+
* stored.
|
|
2218
|
+
*/
|
|
2219
|
+
class ConfigurationUnavailableError extends ServerConfigurationError {
|
|
2220
|
+
constructor(message, options) {
|
|
2221
|
+
super("ClientError::ConfigurationUnavailable", message, options);
|
|
2222
|
+
this.name = "ConfigurationUnavailableError";
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* The deployment answered with a configuration no client can use: an empty or
|
|
2227
|
+
* malformed identifier, an unparseable minimum version, or a chain that is not
|
|
2228
|
+
* a CAIP-2 identifier. Nothing is stored.
|
|
2229
|
+
*/
|
|
2230
|
+
class ConfigurationInvalidError extends ServerConfigurationError {
|
|
2231
|
+
constructor(message, options) {
|
|
2232
|
+
super("ClientError::ConfigurationInvalid", message, options);
|
|
2233
|
+
this.name = "ConfigurationInvalidError";
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* This database is bound to a different deployment. The identifier the backend
|
|
2238
|
+
* published is not the one the database was created against. Recovery is a
|
|
2239
|
+
* database created for the deployment the app now uses.
|
|
2240
|
+
*/
|
|
2241
|
+
class BackendMismatchError extends ServerConfigurationError {
|
|
2242
|
+
constructor(message, options) {
|
|
2243
|
+
super("ClientError::BackendMismatch", message, options);
|
|
2244
|
+
this.name = "BackendMismatchError";
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
/** The deployment requires a newer libxmtp than this SDK is built on. */
|
|
2248
|
+
class ClientVersionTooOldError extends ServerConfigurationError {
|
|
2249
|
+
constructor(message, options) {
|
|
2250
|
+
super("ClientError::ClientVersionTooOld", message, options);
|
|
2251
|
+
this.name = "ClientVersionTooOldError";
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* The deployment requires authentication and no credential source was
|
|
2256
|
+
* configured. The message carries the scopes it requires.
|
|
2257
|
+
*/
|
|
2258
|
+
class AuthRequiredError extends ServerConfigurationError {
|
|
2259
|
+
constructor(message, options) {
|
|
2260
|
+
super("ClientError::AuthRequired", message, options);
|
|
2261
|
+
this.name = "AuthRequiredError";
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* A smart contract wallet signature names a chain the deployment does not
|
|
2266
|
+
* verify on. Raised before any network call, and only for signatures the app
|
|
2267
|
+
* supplied.
|
|
2268
|
+
*/
|
|
2269
|
+
class ChainNotAcceptedError extends ServerConfigurationError {
|
|
2270
|
+
constructor(message, options) {
|
|
2271
|
+
super("ClientError::ChainNotAccepted", message, options);
|
|
2272
|
+
this.name = "ChainNotAcceptedError";
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const configurationCode = /^\[(ClientError::[A-Za-z]+)\]\s*([\s\S]*)$/;
|
|
2276
|
+
const build = (code, message, options) => {
|
|
2277
|
+
switch (code) {
|
|
2278
|
+
case "ClientError::ConfigurationUnavailable":
|
|
2279
|
+
return new ConfigurationUnavailableError(message, options);
|
|
2280
|
+
case "ClientError::ConfigurationInvalid":
|
|
2281
|
+
return new ConfigurationInvalidError(message, options);
|
|
2282
|
+
case "ClientError::BackendMismatch":
|
|
2283
|
+
return new BackendMismatchError(message, options);
|
|
2284
|
+
case "ClientError::ClientVersionTooOld":
|
|
2285
|
+
return new ClientVersionTooOldError(message, options);
|
|
2286
|
+
case "ClientError::AuthRequired":
|
|
2287
|
+
return new AuthRequiredError(message, options);
|
|
2288
|
+
case "ClientError::ChainNotAccepted":
|
|
2289
|
+
return new ChainNotAcceptedError(message, options);
|
|
2290
|
+
default:
|
|
2291
|
+
return undefined;
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
/**
|
|
2295
|
+
* Classifies an error from any SDK call as one of the six configuration
|
|
2296
|
+
* failures, or `undefined` when it is something else.
|
|
2297
|
+
*
|
|
2298
|
+
* Use this where the SDK hands back the raw error, for instance a
|
|
2299
|
+
* `ChainNotAcceptedError` raised deep inside a call that supplies a smart
|
|
2300
|
+
* contract wallet signature.
|
|
2301
|
+
*/
|
|
2302
|
+
const toServerConfigurationError = (error) => {
|
|
2303
|
+
if (!(error instanceof Error))
|
|
2304
|
+
return undefined;
|
|
2305
|
+
const match = configurationCode.exec(error.message);
|
|
2306
|
+
if (!match)
|
|
2307
|
+
return undefined;
|
|
2308
|
+
return build(match[1], match[2], { cause: error });
|
|
2309
|
+
};
|
|
2310
|
+
/**
|
|
2311
|
+
* Rethrows an error as its typed configuration failure, or unchanged when it
|
|
2312
|
+
* is not one.
|
|
2313
|
+
*/
|
|
2314
|
+
const throwServerConfigurationError = (error) => {
|
|
2315
|
+
throw toServerConfigurationError(error) ?? error;
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
const networkOptionKeys = ["env", "backendUrl", "appVersion"];
|
|
1846
2319
|
const hasBackend = (options) => {
|
|
1847
2320
|
return "backend" in options;
|
|
1848
2321
|
};
|
|
1849
2322
|
const resolveBackend$1 = async (options) => {
|
|
1850
2323
|
if (!options) {
|
|
1851
|
-
|
|
2324
|
+
throw new Error("backendUrl is required");
|
|
1852
2325
|
}
|
|
1853
2326
|
if (hasBackend(options)) {
|
|
1854
2327
|
// Validate that no NetworkOptions fields are also set
|
|
@@ -1866,7 +2339,13 @@ const createClient = async (identifier, options) => {
|
|
|
1866
2339
|
const backend = await resolveBackend$1(options);
|
|
1867
2340
|
const inboxId = (await getInboxIdForIdentifier(backend, identifier)) ||
|
|
1868
2341
|
generateInboxId(identifier, options?.nonce);
|
|
1869
|
-
const env =
|
|
2342
|
+
const env = backend.env ?? "default";
|
|
2343
|
+
// `env` only labels the database file, so it must stay one path component.
|
|
2344
|
+
// Without this a value such as "../other" would move the database outside
|
|
2345
|
+
// the working directory.
|
|
2346
|
+
if (env === "" || /[/\\]/.test(env) || env === "." || env === "..") {
|
|
2347
|
+
throw new Error("env must be a single path component");
|
|
2348
|
+
}
|
|
1870
2349
|
let dbPath;
|
|
1871
2350
|
if (options?.dbPath === undefined) {
|
|
1872
2351
|
// Default: auto-generated path
|
|
@@ -1885,6 +2364,8 @@ const createClient = async (identifier, options) => {
|
|
|
1885
2364
|
level: options?.loggingLevel ?? "Off" /* LogLevel.Off */,
|
|
1886
2365
|
stdoutLevel: options?.stdoutLoggingLevel,
|
|
1887
2366
|
otelEndpoint: options?.otelEndpoint,
|
|
2367
|
+
otelServiceName: options?.otelServiceName,
|
|
2368
|
+
otelSampleRatio: options?.otelSampleRatio,
|
|
1888
2369
|
resourceAttributes: options?.resourceAttributes,
|
|
1889
2370
|
};
|
|
1890
2371
|
const deviceSyncWorkerMode = options?.disableDeviceSync
|
|
@@ -1893,6 +2374,11 @@ const createClient = async (identifier, options) => {
|
|
|
1893
2374
|
const dbEncryptionKey = isHexString(options?.dbEncryptionKey)
|
|
1894
2375
|
? Buffer.from(options.dbEncryptionKey.replace(/^0x/, ""), "hex")
|
|
1895
2376
|
: options?.dbEncryptionKey;
|
|
2377
|
+
// Only build the registry when something is actually registered — an empty
|
|
2378
|
+
// one would make the core snapshot app_data on every processed message.
|
|
2379
|
+
const changeCallbacks = options?.unstableChangeCallbacks?.appData
|
|
2380
|
+
? new UnstableChangeCallbacks(options.unstableChangeCallbacks.appData)
|
|
2381
|
+
: undefined;
|
|
1896
2382
|
const client = await createClientWithBackend(backend, {
|
|
1897
2383
|
dbPath: dbPath ?? undefined,
|
|
1898
2384
|
encryptionKey: dbEncryptionKey,
|
|
@@ -1900,23 +2386,25 @@ const createClient = async (identifier, options) => {
|
|
|
1900
2386
|
minDbPoolSize: options?.minDbPoolSize,
|
|
1901
2387
|
useSingleConnection: options?.useSingleConnection,
|
|
1902
2388
|
}, inboxId, identifier, deviceSyncWorkerMode, options?.workerConfig, logOptions, undefined, // allowOffline
|
|
1903
|
-
options?.nonce);
|
|
2389
|
+
options?.nonce, changeCallbacks);
|
|
1904
2390
|
return { client, env };
|
|
1905
2391
|
};
|
|
1906
2392
|
|
|
1907
|
-
/**
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
* @param gatewayHost - Optional gateway host (only used when `envOrBackend` is an `XmtpEnv`)
|
|
1912
|
-
* @returns A `Backend` instance
|
|
1913
|
-
*/
|
|
1914
|
-
const resolveBackend = async (envOrBackend, gatewayHost) => {
|
|
1915
|
-
if (envOrBackend instanceof Backend) {
|
|
1916
|
-
return envOrBackend;
|
|
2393
|
+
/** Resolve a backend from explicit network options or an existing backend. */
|
|
2394
|
+
const resolveBackend = async (optionsOrBackend) => {
|
|
2395
|
+
if (optionsOrBackend instanceof Backend) {
|
|
2396
|
+
return optionsOrBackend;
|
|
1917
2397
|
}
|
|
1918
|
-
return createBackend(
|
|
2398
|
+
return createBackend(optionsOrBackend);
|
|
1919
2399
|
};
|
|
2400
|
+
/**
|
|
2401
|
+
* Read a backend URL and app version from a bare URL, network options, or an
|
|
2402
|
+
* existing backend. `fetchServerConfiguration` builds no backend of its own,
|
|
2403
|
+
* so it needs the two values rather than a `Backend`.
|
|
2404
|
+
*/
|
|
2405
|
+
const resolveEndpoint = (target) => typeof target === "string"
|
|
2406
|
+
? { backendUrl: target }
|
|
2407
|
+
: { backendUrl: target.backendUrl, appVersion: target.appVersion };
|
|
1920
2408
|
const createEphemeralIdentifier = () => ({
|
|
1921
2409
|
identifier: `0x${randomBytes(20).toString("hex")}`,
|
|
1922
2410
|
identifierKind: 0 /* IdentifierKind.Ethereum */,
|
|
@@ -1965,7 +2453,10 @@ class Client {
|
|
|
1965
2453
|
return;
|
|
1966
2454
|
}
|
|
1967
2455
|
this.#identifier = identifier;
|
|
1968
|
-
|
|
2456
|
+
// A build resolves the deployment configuration before any identity work,
|
|
2457
|
+
// so its six failures (spec 006 CFG-041, CFG-044, CFG-052, CFG-060,
|
|
2458
|
+
// CFG-062) surface here and are raised as their own types (CFG-083).
|
|
2459
|
+
const { client, env } = await createClient(identifier, this.#options).catch(throwServerConfigurationError);
|
|
1969
2460
|
this.#client = client;
|
|
1970
2461
|
this.#env = env;
|
|
1971
2462
|
const conversations = this.#client.conversations();
|
|
@@ -1985,7 +2476,7 @@ class Client {
|
|
|
1985
2476
|
const client = new Client(options);
|
|
1986
2477
|
client.#signer = signer;
|
|
1987
2478
|
await client.init(identifier);
|
|
1988
|
-
if (!options
|
|
2479
|
+
if (!options.disableAutoRegister) {
|
|
1989
2480
|
await client.register();
|
|
1990
2481
|
}
|
|
1991
2482
|
return client;
|
|
@@ -2021,12 +2512,12 @@ class Client {
|
|
|
2021
2512
|
return this.#client?.appVersion();
|
|
2022
2513
|
}
|
|
2023
2514
|
/**
|
|
2024
|
-
* Gets the
|
|
2515
|
+
* Gets the label used for the default database file name
|
|
2025
2516
|
*
|
|
2026
2517
|
* @throws {ClientNotInitializedError} if the client is not initialized
|
|
2027
2518
|
*/
|
|
2028
2519
|
get env() {
|
|
2029
|
-
if (
|
|
2520
|
+
if (this.#env === undefined) {
|
|
2030
2521
|
throw new ClientNotInitializedError();
|
|
2031
2522
|
}
|
|
2032
2523
|
return this.#env;
|
|
@@ -2166,7 +2657,12 @@ class Client {
|
|
|
2166
2657
|
const identifier = await finalSigner.getIdentifier();
|
|
2167
2658
|
switch (finalSigner.type) {
|
|
2168
2659
|
case "SCW":
|
|
2169
|
-
|
|
2660
|
+
// The deployment publishes the chains it verifies on, so a signature
|
|
2661
|
+
// for any other chain is rejected here, before any network call
|
|
2662
|
+
// (spec 006 CFG-069, CFG-070).
|
|
2663
|
+
await signatureRequest
|
|
2664
|
+
.addScwSignature(identifier, signature, finalSigner.getChainId(), finalSigner.getBlockNumber?.())
|
|
2665
|
+
.catch(throwServerConfigurationError);
|
|
2170
2666
|
break;
|
|
2171
2667
|
case "EOA":
|
|
2172
2668
|
await signatureRequest.addEcdsaSignature(signature);
|
|
@@ -2406,15 +2902,21 @@ class Client {
|
|
|
2406
2902
|
await this.unsafe_addSignature(signatureRequest);
|
|
2407
2903
|
await this.unsafe_applySignatureRequest(signatureRequest);
|
|
2408
2904
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2905
|
+
/** Revoke installations with an explicit backend or network options. */
|
|
2906
|
+
static async revokeInstallations(signer, inboxId, installationIds, optionsOrBackend) {
|
|
2907
|
+
const backend = await resolveBackend(optionsOrBackend);
|
|
2411
2908
|
const identifier = await signer.getIdentifier();
|
|
2412
2909
|
const signatureRequest = await revokeInstallationsSignatureRequest(backend, identifier, inboxId, installationIds);
|
|
2413
2910
|
const signatureText = await signatureRequest.signatureText();
|
|
2414
2911
|
const signature = await signer.signMessage(signatureText);
|
|
2415
2912
|
switch (signer.type) {
|
|
2416
2913
|
case "SCW":
|
|
2417
|
-
|
|
2914
|
+
// Same chain restriction as the instance path: the deployment's
|
|
2915
|
+
// accepted chains reject any other one here, before the network call
|
|
2916
|
+
// (spec 006 CFG-069, CFG-070).
|
|
2917
|
+
await signatureRequest
|
|
2918
|
+
.addScwSignature(identifier, signature, signer.getChainId(), signer.getBlockNumber?.())
|
|
2919
|
+
.catch(throwServerConfigurationError);
|
|
2418
2920
|
break;
|
|
2419
2921
|
case "EOA":
|
|
2420
2922
|
await signatureRequest.addEcdsaSignature(signature);
|
|
@@ -2537,12 +3039,14 @@ class Client {
|
|
|
2537
3039
|
return false;
|
|
2538
3040
|
}
|
|
2539
3041
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
3042
|
+
/** Fetch inbox states with an explicit backend or network options. */
|
|
3043
|
+
static async fetchInboxStates(inboxIds, optionsOrBackend) {
|
|
3044
|
+
const backend = await resolveBackend(optionsOrBackend);
|
|
2542
3045
|
return fetchInboxStatesByInboxIds(backend, inboxIds);
|
|
2543
3046
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
3047
|
+
/** Fetch inbox update counts with an explicit backend or network options. */
|
|
3048
|
+
static async fetchLatestInboxUpdatesCount(inboxIds, optionsOrBackend) {
|
|
3049
|
+
const backend = await resolveBackend(optionsOrBackend);
|
|
2546
3050
|
// The node-bindings Client is a napi-rs class with no explicit close/free
|
|
2547
3051
|
// method. Release is non-deterministic: once this reference goes out of
|
|
2548
3052
|
// scope, JS GC will eventually invoke the Rust Drop impl and reclaim the
|
|
@@ -2556,9 +3060,9 @@ class Client {
|
|
|
2556
3060
|
const result = await client.fetchInboxUpdatesCount(inboxIds, true);
|
|
2557
3061
|
return toInboxUpdatesCountMap(result);
|
|
2558
3062
|
}
|
|
2559
|
-
|
|
2560
|
-
static async canMessage(identifiers,
|
|
2561
|
-
const backend = await resolveBackend(
|
|
3063
|
+
/** Check identifiers with an explicit backend or network options. */
|
|
3064
|
+
static async canMessage(identifiers, optionsOrBackend) {
|
|
3065
|
+
const backend = await resolveBackend(optionsOrBackend);
|
|
2562
3066
|
const canMessageMap = new Map();
|
|
2563
3067
|
for (const identifier of identifiers) {
|
|
2564
3068
|
const inboxId = await getInboxIdForIdentifier(backend, identifier);
|
|
@@ -2583,12 +3087,14 @@ class Client {
|
|
|
2583
3087
|
return false;
|
|
2584
3088
|
}
|
|
2585
3089
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
3090
|
+
/** Check address authorization with an explicit backend or network options. */
|
|
3091
|
+
static async isAddressAuthorized(inboxId, address, optionsOrBackend) {
|
|
3092
|
+
const backend = await resolveBackend(optionsOrBackend);
|
|
2588
3093
|
return await isAddressAuthorized(backend, inboxId, address);
|
|
2589
3094
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
3095
|
+
/** Check installation authorization with an explicit backend or network options. */
|
|
3096
|
+
static async isInstallationAuthorized(inboxId, installation, optionsOrBackend) {
|
|
3097
|
+
const backend = await resolveBackend(optionsOrBackend);
|
|
2592
3098
|
return await isInstallationAuthorized(backend, inboxId, installation);
|
|
2593
3099
|
}
|
|
2594
3100
|
/**
|
|
@@ -2603,75 +3109,6 @@ class Client {
|
|
|
2603
3109
|
excludeDisappearingMessages: false,
|
|
2604
3110
|
};
|
|
2605
3111
|
}
|
|
2606
|
-
/**
|
|
2607
|
-
* Get the default server URL based on the environment
|
|
2608
|
-
*/
|
|
2609
|
-
#getDefaultServerUrl() {
|
|
2610
|
-
const env = this.env;
|
|
2611
|
-
return HistorySyncUrls[env];
|
|
2612
|
-
}
|
|
2613
|
-
/**
|
|
2614
|
-
* Send a sync request to other devices on the network
|
|
2615
|
-
*
|
|
2616
|
-
* @param options - Archive options specifying what to sync (defaults to consent and messages)
|
|
2617
|
-
* @param serverUrl - The server URL for the sync request (defaults to environment-specific URL)
|
|
2618
|
-
* @returns Promise that resolves when the sync request is sent
|
|
2619
|
-
*/
|
|
2620
|
-
async sendSyncRequest(options, serverUrl) {
|
|
2621
|
-
if (!this.#client) {
|
|
2622
|
-
throw new ClientNotInitializedError();
|
|
2623
|
-
}
|
|
2624
|
-
const resolvedOptions = options ?? this.#getDefaultArchiveOptions();
|
|
2625
|
-
const resolvedServerUrl = serverUrl ?? this.#getDefaultServerUrl();
|
|
2626
|
-
return this.#client
|
|
2627
|
-
.deviceSync()
|
|
2628
|
-
.sendSyncRequest(resolvedOptions, resolvedServerUrl);
|
|
2629
|
-
}
|
|
2630
|
-
/**
|
|
2631
|
-
* Send a sync archive to the sync group
|
|
2632
|
-
*
|
|
2633
|
-
* @param pin - The pin used for reference when importing
|
|
2634
|
-
* @param options - Archive options specifying what to sync (defaults to consent and messages)
|
|
2635
|
-
* @param serverUrl - The server URL for the sync archive (defaults to environment-specific URL)
|
|
2636
|
-
* @returns Promise that resolves when the sync archive is sent
|
|
2637
|
-
*/
|
|
2638
|
-
async sendSyncArchive(pin, options, serverUrl) {
|
|
2639
|
-
if (!this.#client) {
|
|
2640
|
-
throw new ClientNotInitializedError();
|
|
2641
|
-
}
|
|
2642
|
-
const resolvedOptions = options ?? this.#getDefaultArchiveOptions();
|
|
2643
|
-
const resolvedServerUrl = serverUrl ?? this.#getDefaultServerUrl();
|
|
2644
|
-
return this.#client
|
|
2645
|
-
.deviceSync()
|
|
2646
|
-
.sendSyncArchive(resolvedOptions, resolvedServerUrl, pin);
|
|
2647
|
-
}
|
|
2648
|
-
/**
|
|
2649
|
-
* Process a sync archive that matches the pin given
|
|
2650
|
-
*
|
|
2651
|
-
* @param archivePin - Optional pin to match. If not provided, processes the last archive sent
|
|
2652
|
-
* @returns Promise that resolves when the archive is processed
|
|
2653
|
-
*/
|
|
2654
|
-
async processSyncArchive(archivePin) {
|
|
2655
|
-
if (!this.#client) {
|
|
2656
|
-
throw new ClientNotInitializedError();
|
|
2657
|
-
}
|
|
2658
|
-
return this.#client.deviceSync().processSyncArchive(archivePin);
|
|
2659
|
-
}
|
|
2660
|
-
/**
|
|
2661
|
-
* List the archives available for import in the sync group
|
|
2662
|
-
*
|
|
2663
|
-
* You may need to manually sync the sync group before calling
|
|
2664
|
-
* this function to see recently uploaded archives.
|
|
2665
|
-
*
|
|
2666
|
-
* @param daysCutoff - Number of days to look back for archives
|
|
2667
|
-
* @returns Array of available archive information
|
|
2668
|
-
*/
|
|
2669
|
-
listAvailableArchives(daysCutoff) {
|
|
2670
|
-
if (!this.#client) {
|
|
2671
|
-
throw new ClientNotInitializedError();
|
|
2672
|
-
}
|
|
2673
|
-
return this.#client.deviceSync().listAvailableArchives(daysCutoff);
|
|
2674
|
-
}
|
|
2675
3112
|
/**
|
|
2676
3113
|
* Archive application elements to file for later restoration
|
|
2677
3114
|
*
|
|
@@ -2726,7 +3163,230 @@ class Client {
|
|
|
2726
3163
|
}
|
|
2727
3164
|
return this.#client.deviceSync().syncAllDeviceSyncGroups();
|
|
2728
3165
|
}
|
|
3166
|
+
/** Enable notifications and register the delivery channel. */
|
|
3167
|
+
async enableNotifications(config) {
|
|
3168
|
+
if (!this.#client)
|
|
3169
|
+
throw new ClientNotInitializedError();
|
|
3170
|
+
try {
|
|
3171
|
+
return toNotificationState(await this.#client.enableNotifications(toBindingNotificationConfig(config)));
|
|
3172
|
+
}
|
|
3173
|
+
catch (error) {
|
|
3174
|
+
return throwNotificationError(error);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
/** Disable notifications. Per-conversation overrides remain stored. */
|
|
3178
|
+
async disableNotifications() {
|
|
3179
|
+
if (!this.#client)
|
|
3180
|
+
throw new ClientNotInitializedError();
|
|
3181
|
+
try {
|
|
3182
|
+
await this.#client.disableNotifications();
|
|
3183
|
+
}
|
|
3184
|
+
catch (error) {
|
|
3185
|
+
throwNotificationError(error);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
/** Read the local state without a backend request. */
|
|
3189
|
+
notificationState() {
|
|
3190
|
+
return Promise.resolve().then(() => {
|
|
3191
|
+
if (!this.#client)
|
|
3192
|
+
throw new ClientNotInitializedError();
|
|
3193
|
+
return toNotificationState(this.#client.notificationState());
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* What the deployment published about itself, as this client resolved it
|
|
3198
|
+
* when it was built (spec 006 CFG-030, CFG-080).
|
|
3199
|
+
*
|
|
3200
|
+
* Reads memory and makes no backend request. The value never changes for the
|
|
3201
|
+
* life of the client: a background refresh rewrites the stored copy, and a
|
|
3202
|
+
* new value takes effect at the next build.
|
|
3203
|
+
*
|
|
3204
|
+
* @throws {ClientNotInitializedError} if the client is not initialized
|
|
3205
|
+
*/
|
|
3206
|
+
serverConfiguration() {
|
|
3207
|
+
if (!this.#client) {
|
|
3208
|
+
throw new ClientNotInitializedError();
|
|
3209
|
+
}
|
|
3210
|
+
return this.#client.serverConfiguration();
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Fetch the deployment configuration now, rewrite the stored copy, and
|
|
3214
|
+
* return what was fetched (spec 006 CFG-082).
|
|
3215
|
+
*
|
|
3216
|
+
* The snapshot this client holds is unchanged; use it to observe a change an
|
|
3217
|
+
* operator has made without restarting.
|
|
3218
|
+
*
|
|
3219
|
+
* @throws {ClientNotInitializedError} if the client is not initialized
|
|
3220
|
+
* @throws {ConfigurationUnavailableError} if the deployment did not answer
|
|
3221
|
+
* @throws {ConfigurationInvalidError} if it published an unusable configuration
|
|
3222
|
+
* @throws {BackendMismatchError} if it published a different identifier
|
|
3223
|
+
* @throws {ClientVersionTooOldError} if it now requires a newer libxmtp
|
|
3224
|
+
*/
|
|
3225
|
+
async refreshServerConfiguration() {
|
|
3226
|
+
if (!this.#client) {
|
|
3227
|
+
throw new ClientNotInitializedError();
|
|
3228
|
+
}
|
|
3229
|
+
return this.#client
|
|
3230
|
+
.refreshServerConfiguration()
|
|
3231
|
+
.catch(throwServerConfigurationError);
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Read what a deployment publishes about itself with no database, no client,
|
|
3235
|
+
* and no credential (spec 006 CFG-081, CFG-045).
|
|
3236
|
+
*
|
|
3237
|
+
* Lets an app learn `auth.enabled`, `auth.requiredScopes`, and the accepted
|
|
3238
|
+
* smart contract wallet chains before it decides how to build a client.
|
|
3239
|
+
* Nothing is stored and no identifier binding is applied.
|
|
3240
|
+
*
|
|
3241
|
+
* @param target - A backend URL, network options, or an existing backend
|
|
3242
|
+
* @throws {ConfigurationUnavailableError} if the deployment did not answer
|
|
3243
|
+
* @throws {ConfigurationInvalidError} if it published an unusable configuration
|
|
3244
|
+
*/
|
|
3245
|
+
static async fetchServerConfiguration(target) {
|
|
3246
|
+
const { backendUrl, appVersion } = resolveEndpoint(target);
|
|
3247
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Validate options from JavaScript callers.
|
|
3248
|
+
if (!backendUrl?.trim()) {
|
|
3249
|
+
throw new Error("backendUrl is required");
|
|
3250
|
+
}
|
|
3251
|
+
return fetchServerConfiguration(backendUrl, appVersion).catch(throwServerConfigurationError);
|
|
3252
|
+
}
|
|
2729
3253
|
}
|
|
2730
3254
|
|
|
2731
|
-
|
|
3255
|
+
const streamFailureMarker = "\n[XMTP_STREAM_FAILURE_V1]";
|
|
3256
|
+
const maxCursor = (1n << 64n) - 1n;
|
|
3257
|
+
/**
|
|
3258
|
+
* Reads structured progress from a stream, sync, or publication error.
|
|
3259
|
+
* All sequence values are exact bigints. Invalid or absent details return undefined.
|
|
3260
|
+
* A published-but-unconfirmed error does not mean that publication failed.
|
|
3261
|
+
*/
|
|
3262
|
+
const getStreamFailureDetails = (error) => {
|
|
3263
|
+
try {
|
|
3264
|
+
const message = typeof error === "string" ? error : readObject(error).message;
|
|
3265
|
+
if (typeof message !== "string")
|
|
3266
|
+
return undefined;
|
|
3267
|
+
const marker = message.lastIndexOf(streamFailureMarker);
|
|
3268
|
+
if (marker === -1)
|
|
3269
|
+
return undefined;
|
|
3270
|
+
const details = readObject(JSON.parse(message.slice(marker + streamFailureMarker.length)));
|
|
3271
|
+
return {
|
|
3272
|
+
kind: readChoice(details.kind, [
|
|
3273
|
+
"barrier",
|
|
3274
|
+
"published_but_unconfirmed",
|
|
3275
|
+
"catch_up",
|
|
3276
|
+
]),
|
|
3277
|
+
code: readString(details.code),
|
|
3278
|
+
message: readString(details.message),
|
|
3279
|
+
retryable: readBoolean(details.retryable),
|
|
3280
|
+
intentId: readNullable(details.intentId, readIntentId),
|
|
3281
|
+
publishedIntentIds: readArray(details.publishedIntentIds, readIntentId),
|
|
3282
|
+
summary: readNullable(details.summary, (value) => {
|
|
3283
|
+
const summary = readObject(value);
|
|
3284
|
+
return {
|
|
3285
|
+
messages: readCursor(summary.messages),
|
|
3286
|
+
conversations: readCursor(summary.conversations),
|
|
3287
|
+
failed: readCursor(summary.failed),
|
|
3288
|
+
completed: readBoolean(summary.completed),
|
|
3289
|
+
};
|
|
3290
|
+
}),
|
|
3291
|
+
barriers: readArray(details.barriers, readBarrier),
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
catch {
|
|
3295
|
+
return undefined;
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
const readBarrier = (value) => {
|
|
3299
|
+
const barrier = readObject(value);
|
|
3300
|
+
return {
|
|
3301
|
+
reason: readChoice(barrier.reason, ["blocked", "deadline", "cancelled"]),
|
|
3302
|
+
unfinished: readArray(barrier.unfinished, (value) => {
|
|
3303
|
+
const topic = readObject(value);
|
|
3304
|
+
return {
|
|
3305
|
+
topic: readTopic(topic.topic),
|
|
3306
|
+
target: readNullable(topic.target, readCursor),
|
|
3307
|
+
received: readCursor(topic.received),
|
|
3308
|
+
processed: readCursor(topic.processed),
|
|
3309
|
+
unresolvedWelcomes: readArray(topic.unresolvedWelcomes, readCursor),
|
|
3310
|
+
inactive: readBoolean(topic.inactive),
|
|
3311
|
+
cause: readNullable(topic.cause, (value) => {
|
|
3312
|
+
const cause = readObject(value);
|
|
3313
|
+
return {
|
|
3314
|
+
kind: readChoice(cause.kind, [
|
|
3315
|
+
"target_pending",
|
|
3316
|
+
"receipt_pending",
|
|
3317
|
+
"processing_pending",
|
|
3318
|
+
"blocked",
|
|
3319
|
+
"storage",
|
|
3320
|
+
"receiver",
|
|
3321
|
+
"invalid_topic",
|
|
3322
|
+
]),
|
|
3323
|
+
code: readNullable(cause.code, readString),
|
|
3324
|
+
message: readString(cause.message),
|
|
3325
|
+
retryable: readBoolean(cause.retryable),
|
|
3326
|
+
};
|
|
3327
|
+
}),
|
|
3328
|
+
};
|
|
3329
|
+
}),
|
|
3330
|
+
};
|
|
3331
|
+
};
|
|
3332
|
+
const readObject = (value) => {
|
|
3333
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3334
|
+
throw new Error("Invalid stream failure object");
|
|
3335
|
+
}
|
|
3336
|
+
return value;
|
|
3337
|
+
};
|
|
3338
|
+
const readString = (value) => {
|
|
3339
|
+
if (typeof value !== "string")
|
|
3340
|
+
throw new Error("Invalid stream failure text");
|
|
3341
|
+
return value;
|
|
3342
|
+
};
|
|
3343
|
+
const readBoolean = (value) => {
|
|
3344
|
+
if (typeof value !== "boolean")
|
|
3345
|
+
throw new Error("Invalid stream failure flag");
|
|
3346
|
+
return value;
|
|
3347
|
+
};
|
|
3348
|
+
const readChoice = (value, choices) => {
|
|
3349
|
+
const choice = choices.find((choice) => choice === value);
|
|
3350
|
+
if (choice === undefined)
|
|
3351
|
+
throw new Error("Invalid stream failure kind");
|
|
3352
|
+
return choice;
|
|
3353
|
+
};
|
|
3354
|
+
const readNullable = (value, read) => (value === null ? null : read(value));
|
|
3355
|
+
const readArray = (value, read) => {
|
|
3356
|
+
if (!Array.isArray(value))
|
|
3357
|
+
throw new Error("Invalid stream failure list");
|
|
3358
|
+
return value.map(read);
|
|
3359
|
+
};
|
|
3360
|
+
const readCursor = (value) => {
|
|
3361
|
+
const cursor = readString(value);
|
|
3362
|
+
if (!/^(0|[1-9][0-9]*)$/.test(cursor)) {
|
|
3363
|
+
throw new Error("Invalid stream failure sequence");
|
|
3364
|
+
}
|
|
3365
|
+
const number = BigInt(cursor);
|
|
3366
|
+
if (number > maxCursor)
|
|
3367
|
+
throw new Error("Stream failure sequence is too large");
|
|
3368
|
+
return number;
|
|
3369
|
+
};
|
|
3370
|
+
const readIntentId = (value) => {
|
|
3371
|
+
if (typeof value !== "number" ||
|
|
3372
|
+
!Number.isInteger(value) ||
|
|
3373
|
+
value < -2147483648 ||
|
|
3374
|
+
value > 2147483647) {
|
|
3375
|
+
throw new Error("Invalid stream failure intent ID");
|
|
3376
|
+
}
|
|
3377
|
+
return value;
|
|
3378
|
+
};
|
|
3379
|
+
const readTopic = (value) => {
|
|
3380
|
+
const hex = readString(value);
|
|
3381
|
+
if (!/^(?:[0-9a-fA-F]{2})*$/.test(hex)) {
|
|
3382
|
+
throw new Error("Invalid stream failure topic");
|
|
3383
|
+
}
|
|
3384
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3385
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
3386
|
+
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
3387
|
+
}
|
|
3388
|
+
return bytes;
|
|
3389
|
+
};
|
|
3390
|
+
|
|
3391
|
+
export { AccountAlreadyAssociatedError, AuthRequiredError, BackendMismatchError, ChainNotAcceptedError, Client, ClientNotInitializedError, ClientVersionTooOldError, CodecRegistry, ConfigurationInvalidError, ConfigurationUnavailableError, Conversation, Conversations, DebugInformation, DecodedMessage, Dm, Group, InboxReassignError, MessageStream, MissingContentTypeError, NotificationError, Preferences, ServerConfigurationError, SignerUnavailableError, StreamFailedError, StreamInvalidRetryAttemptsError, createBackend, generateInboxId, getInboxIdForIdentifier, getStreamFailureDetails, isActions, isAttachment, isGroupUpdated, isHexString, isIntent, isLeaveRequest, isMarkdown, isMultiRemoteAttachment, isReaction, isReadReceipt, isRemoteAttachment, isReply, isText, isTextReply, isTransactionReference, isWalletSendCalls, throwServerConfigurationError, toServerConfigurationError, validHex };
|
|
2732
3392
|
//# sourceMappingURL=index.js.map
|