@xmtp/browser-sdk 3.1.2 → 4.0.1

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.
@@ -11,6 +11,7 @@ import type {
11
11
  StreamAction,
12
12
  StreamActionErrorData,
13
13
  } from "@/types/actions/streams";
14
+ import type { StreamOptions } from "@/utils/streams";
14
15
 
15
16
  const handleError = (event: ErrorEvent) => {
16
17
  console.error(event.message);
@@ -111,17 +112,24 @@ export class ClientWorkerClass {
111
112
  * @param callback - The callback to handle the stream message
112
113
  * @returns A function to remove the stream handler
113
114
  */
114
- handleStreamMessage = <T extends StreamAction["result"]>(
115
+ handleStreamMessage = <T extends StreamAction["result"], V = T>(
115
116
  streamId: string,
116
- callback: (error: Error | null, value: T | null) => void,
117
+ callback: (error: Error | null, value: T | undefined) => void,
118
+ options?: StreamOptions<T, V>,
117
119
  ) => {
118
120
  const streamHandler = (
119
121
  event: MessageEvent<StreamAction | StreamActionErrorData>,
120
122
  ) => {
121
123
  const eventData = event.data;
124
+ // only handle messages for the passed stream ID
122
125
  if (eventData.streamId === streamId) {
126
+ // if the stream failed, call the onFail callback
127
+ if (eventData.action === "stream.fail") {
128
+ options?.onFail?.();
129
+ return;
130
+ }
123
131
  if ("error" in eventData) {
124
- callback(eventData.error, null);
132
+ callback(eventData.error, undefined);
125
133
  } else {
126
134
  callback(null, eventData.result as T);
127
135
  }
@@ -129,7 +137,10 @@ export class ClientWorkerClass {
129
137
  };
130
138
  this.#worker.addEventListener("message", streamHandler);
131
139
 
132
- return () => {
140
+ return async () => {
141
+ await this.sendMessage("endStream", {
142
+ streamId,
143
+ });
133
144
  this.#worker.removeEventListener("message", streamHandler);
134
145
  };
135
146
  };
@@ -2,7 +2,6 @@ import type { ContentTypeId } from "@xmtp/content-type-primitives";
2
2
  import { ContentTypeText } from "@xmtp/content-type-text";
3
3
  import type { ConsentState } from "@xmtp/wasm-bindings";
4
4
  import { v4 } from "uuid";
5
- import { AsyncStream, type StreamCallback } from "@/AsyncStream";
6
5
  import type { Client } from "@/Client";
7
6
  import { DecodedMessage } from "@/DecodedMessage";
8
7
  import type {
@@ -12,6 +11,11 @@ import type {
12
11
  } from "@/utils/conversions";
13
12
  import { nsToDate } from "@/utils/date";
14
13
  import { MissingContentTypeError } from "@/utils/errors";
14
+ import {
15
+ createStream,
16
+ type StreamCallback,
17
+ type StreamOptions,
18
+ } from "@/utils/streams";
15
19
 
16
20
  /**
17
21
  * Represents a conversation
@@ -264,38 +268,35 @@ export class Conversation<ContentTypes = unknown> {
264
268
  * @param callback - Optional callback function for handling new stream values
265
269
  * @returns Stream instance for new messages
266
270
  */
267
- async stream(callback?: StreamCallback<DecodedMessage<ContentTypes>>) {
268
- const streamId = v4();
269
- const asyncStream = new AsyncStream<DecodedMessage<ContentTypes>>();
270
- const endStream = this.#client.handleStreamMessage<SafeMessage>(
271
- streamId,
272
- (error, value) => {
273
- let err: Error | null = error;
274
- let message: DecodedMessage<ContentTypes> | undefined;
275
-
276
- if (value) {
277
- try {
278
- message = new DecodedMessage(this.#client, value);
279
- } catch (error) {
280
- err = error as Error;
281
- }
282
- }
283
-
284
- void asyncStream.callback(err, message);
285
- void callback?.(err, message);
286
- },
287
- );
288
- await this.#client.sendMessage("conversation.stream", {
289
- groupId: this.#id,
290
- streamId,
291
- });
292
- asyncStream.onDone = () => {
293
- void this.#client.sendMessage("endStream", {
271
+ async stream(
272
+ options?: StreamOptions<SafeMessage, DecodedMessage<ContentTypes>>,
273
+ ) {
274
+ const stream = async (
275
+ callback: StreamCallback<SafeMessage>,
276
+ onFail: () => void,
277
+ ) => {
278
+ const streamId = v4();
279
+ // sync the conversation
280
+ await this.sync();
281
+ // start the stream
282
+ await this.#client.sendMessage("conversation.stream", {
283
+ groupId: this.#id,
294
284
  streamId,
295
285
  });
296
- endStream();
286
+ // handle stream messages
287
+ return this.#client.handleStreamMessage<
288
+ SafeMessage,
289
+ DecodedMessage<ContentTypes>
290
+ >(streamId, callback, {
291
+ ...options,
292
+ onFail,
293
+ });
297
294
  };
298
- return asyncStream;
295
+ const convertMessage = (value: SafeMessage) => {
296
+ return new DecodedMessage(this.#client, value);
297
+ };
298
+
299
+ return createStream(stream, convertMessage, options);
299
300
  }
300
301
 
301
302
  async pausedForVersion() {
@@ -4,7 +4,6 @@ import {
4
4
  type Identifier,
5
5
  } from "@xmtp/wasm-bindings";
6
6
  import { v4 } from "uuid";
7
- import { AsyncStream, type StreamCallback } from "@/AsyncStream";
8
7
  import type { Client } from "@/Client";
9
8
  import { DecodedMessage } from "@/DecodedMessage";
10
9
  import { Dm } from "@/Dm";
@@ -16,6 +15,11 @@ import type {
16
15
  SafeListConversationsOptions,
17
16
  SafeMessage,
18
17
  } from "@/utils/conversions";
18
+ import {
19
+ createStream,
20
+ type StreamCallback,
21
+ type StreamOptions,
22
+ } from "@/utils/streams";
19
23
 
20
24
  /**
21
25
  * Manages conversations
@@ -285,144 +289,154 @@ export class Conversations<ContentTypes = unknown> {
285
289
  /**
286
290
  * Creates a stream for new conversations
287
291
  *
288
- * @param callback - Optional callback function for handling new stream value
289
- * @param conversationType - Optional type to filter conversations
292
+ * @param options - Optional stream options
293
+ * @param options.conversationType - Optional type to filter conversations
290
294
  * @returns Stream instance for new conversations
291
295
  */
292
296
  async stream<
293
297
  T extends Group<ContentTypes> | Dm<ContentTypes> =
294
298
  | Group<ContentTypes>
295
299
  | Dm<ContentTypes>,
296
- >(callback?: StreamCallback<T>, conversationType?: ConversationType) {
297
- const streamId = v4();
298
- const asyncStream = new AsyncStream<T>();
299
- const endStream = this.#client.handleStreamMessage<SafeConversation>(
300
- streamId,
301
- (error, value) => {
302
- let err: Error | null = error;
303
- let streamValue: T | undefined;
304
-
305
- if (value) {
306
- try {
307
- streamValue =
308
- value.metadata.conversationType === "group"
309
- ? (new Group(this.#client, value.id, value) as T)
310
- : (new Dm(this.#client, value.id, value) as T);
311
- } catch (error) {
312
- err = error as Error;
313
- }
314
- }
315
-
316
- void asyncStream.callback(err, streamValue);
317
- void callback?.(err, streamValue);
318
- },
319
- );
320
- await this.#client.sendMessage("conversations.stream", {
321
- streamId,
322
- conversationType,
323
- });
324
- asyncStream.onDone = () => {
325
- void this.#client.sendMessage("endStream", {
300
+ >(
301
+ options?: StreamOptions<SafeConversation, T> & {
302
+ conversationType?: ConversationType;
303
+ },
304
+ ) {
305
+ const stream = async (
306
+ callback: StreamCallback<SafeConversation>,
307
+ onFail: () => void,
308
+ ) => {
309
+ const streamId = v4();
310
+ // sync the conversation
311
+ await this.sync();
312
+ // start the stream
313
+ await this.#client.sendMessage("conversations.stream", {
326
314
  streamId,
315
+ conversationType: options?.conversationType,
327
316
  });
328
- endStream();
317
+ // handle stream messages
318
+ return this.#client.handleStreamMessage<SafeConversation, T>(
319
+ streamId,
320
+ callback,
321
+ {
322
+ ...options,
323
+ onFail,
324
+ },
325
+ );
326
+ };
327
+ const convertConversation = (value: SafeConversation) => {
328
+ return value.metadata.conversationType === "group"
329
+ ? (new Group(this.#client, value.id, value) as T)
330
+ : (new Dm(this.#client, value.id, value) as T);
329
331
  };
330
- return asyncStream;
332
+
333
+ return createStream(stream, convertConversation, options);
331
334
  }
332
335
 
333
336
  /**
334
337
  * Creates a stream for new group conversations
335
338
  *
336
- * @param callback - Optional callback function for handling new stream value
339
+ * @param options - Optional stream options
337
340
  * @returns Stream instance for new group conversations
338
341
  */
339
- async streamGroups(callback?: StreamCallback<Group<ContentTypes>>) {
340
- return this.stream(callback, ConversationType.Group);
342
+ async streamGroups(
343
+ options?: StreamOptions<SafeConversation, Group<ContentTypes>>,
344
+ ) {
345
+ return this.stream({
346
+ ...options,
347
+ conversationType: ConversationType.Group,
348
+ });
341
349
  }
342
350
 
343
351
  /**
344
352
  * Creates a stream for new DM conversations
345
353
  *
346
- * @param callback - Optional callback function for handling new stream value
354
+ * @param options - Optional stream options
347
355
  * @returns Stream instance for new DM conversations
348
356
  */
349
- async streamDms(callback?: StreamCallback<Dm<ContentTypes>>) {
350
- return this.stream(callback, ConversationType.Dm);
357
+ async streamDms(options?: StreamOptions<SafeConversation, Dm<ContentTypes>>) {
358
+ return this.stream({
359
+ ...options,
360
+ conversationType: ConversationType.Dm,
361
+ });
351
362
  }
352
363
 
353
364
  /**
354
365
  * Creates a stream for all new messages
355
366
  *
356
- * @param callback - Optional callback function for handling new stream value
357
- * @param conversationType - Optional conversation type to filter messages
367
+ * @param options - Optional stream options
368
+ * @param options.conversationType - Optional conversation type to filter messages
369
+ * @param options.consentStates - Optional consent states to filter messages
358
370
  * @returns Stream instance for new messages
359
371
  */
360
372
  async streamAllMessages(
361
- callback?: StreamCallback<DecodedMessage<ContentTypes>>,
362
- conversationType?: ConversationType,
363
- consentStates?: ConsentState[],
373
+ options?: StreamOptions<SafeMessage, DecodedMessage<ContentTypes>> & {
374
+ conversationType?: ConversationType;
375
+ consentStates?: ConsentState[];
376
+ },
364
377
  ) {
365
- const streamId = v4();
366
- const asyncStream = new AsyncStream<DecodedMessage<ContentTypes>>();
367
- const endStream = this.#client.handleStreamMessage<SafeMessage>(
368
- streamId,
369
- (error, value) => {
370
- let err: Error | null = error;
371
- let message: DecodedMessage<ContentTypes> | undefined;
372
-
373
- if (value) {
374
- try {
375
- message = new DecodedMessage(this.#client, value);
376
- } catch (error) {
377
- err = error as Error;
378
- }
379
- }
380
-
381
- void asyncStream.callback(err, message);
382
- void callback?.(err, message);
383
- },
384
- );
385
- await this.#client.sendMessage("conversations.streamAllMessages", {
386
- streamId,
387
- conversationType,
388
- consentStates,
389
- });
390
- asyncStream.onDone = () => {
391
- void this.#client.sendMessage("endStream", {
378
+ const stream = async (
379
+ callback: StreamCallback<SafeMessage>,
380
+ onFail: () => void,
381
+ ) => {
382
+ const streamId = v4();
383
+ // sync the conversation
384
+ await this.sync();
385
+ // start the stream
386
+ await this.#client.sendMessage("conversations.streamAllMessages", {
392
387
  streamId,
388
+ conversationType: options?.conversationType,
389
+ consentStates: options?.consentStates,
390
+ });
391
+ // handle stream messages
392
+ return this.#client.handleStreamMessage<
393
+ SafeMessage,
394
+ DecodedMessage<ContentTypes>
395
+ >(streamId, callback, {
396
+ ...options,
397
+ onFail,
393
398
  });
394
- endStream();
395
399
  };
396
- return asyncStream;
400
+ const convertMessage = (value: SafeMessage) => {
401
+ return new DecodedMessage(this.#client, value);
402
+ };
403
+
404
+ return createStream(stream, convertMessage, options);
397
405
  }
398
406
 
399
407
  /**
400
408
  * Creates a stream for all new group messages
401
409
  *
402
- * @param callback - Optional callback function for handling new stream value
410
+ * @param options - Optional stream options
411
+ * @param options.consentStates - Optional consent states to filter messages
403
412
  * @returns Stream instance for new group messages
404
413
  */
405
414
  async streamAllGroupMessages(
406
- callback?: StreamCallback<DecodedMessage<ContentTypes>>,
407
- consentStates?: ConsentState[],
415
+ options?: StreamOptions<SafeMessage, DecodedMessage<ContentTypes>> & {
416
+ consentStates?: ConsentState[];
417
+ },
408
418
  ) {
409
- return this.streamAllMessages(
410
- callback,
411
- ConversationType.Group,
412
- consentStates,
413
- );
419
+ return this.streamAllMessages({
420
+ ...options,
421
+ conversationType: ConversationType.Group,
422
+ });
414
423
  }
415
424
 
416
425
  /**
417
426
  * Creates a stream for all new DM messages
418
427
  *
419
- * @param callback - Optional callback function for handling new stream value
428
+ * @param options - Optional stream options
429
+ * @param options.consentStates - Optional consent states to filter messages
420
430
  * @returns Stream instance for new DM messages
421
431
  */
422
432
  async streamAllDmMessages(
423
- callback?: StreamCallback<DecodedMessage<ContentTypes>>,
424
- consentStates?: ConsentState[],
433
+ options?: StreamOptions<SafeMessage, DecodedMessage<ContentTypes>> & {
434
+ consentStates?: ConsentState[];
435
+ },
425
436
  ) {
426
- return this.streamAllMessages(callback, ConversationType.Dm, consentStates);
437
+ return this.streamAllMessages({
438
+ ...options,
439
+ conversationType: ConversationType.Dm,
440
+ });
427
441
  }
428
442
  }
@@ -1,7 +1,11 @@
1
1
  import type { ConsentEntityType, UserPreference } from "@xmtp/wasm-bindings";
2
2
  import { v4 } from "uuid";
3
- import { AsyncStream, type StreamCallback } from "@/AsyncStream";
4
3
  import type { SafeConsent } from "@/utils/conversions";
4
+ import {
5
+ createStream,
6
+ type StreamCallback,
7
+ type StreamOptions,
8
+ } from "@/utils/streams";
5
9
  import type { Client } from "./Client";
6
10
 
7
11
  /**
@@ -95,56 +99,64 @@ export class Preferences<ContentTypes = unknown> {
95
99
  /**
96
100
  * Creates a stream of consent state updates
97
101
  *
98
- * @param callback - Optional callback function for handling stream updates
102
+ * @param options - Optional stream options
99
103
  * @returns Stream instance for consent updates
100
104
  */
101
- async streamConsent(callback?: StreamCallback<SafeConsent[]>) {
102
- const streamId = v4();
103
- const asyncStream = new AsyncStream<SafeConsent[]>();
104
- const endStream = this.#client.handleStreamMessage<SafeConsent[]>(
105
- streamId,
106
- (error, value) => {
107
- void asyncStream.callback(error, value ?? undefined);
108
- void callback?.(error, value ?? undefined);
109
- },
110
- );
111
- await this.#client.sendMessage("preferences.streamConsent", {
112
- streamId,
113
- });
114
- asyncStream.onDone = () => {
115
- void this.#client.sendMessage("endStream", {
105
+ async streamConsent(options?: StreamOptions<SafeConsent[]>) {
106
+ const stream = async (
107
+ callback: StreamCallback<SafeConsent[]>,
108
+ onFail: () => void,
109
+ ) => {
110
+ const streamId = v4();
111
+ // sync the conversation
112
+ await this.sync();
113
+ // start the stream
114
+ await this.#client.sendMessage("preferences.streamConsent", {
116
115
  streamId,
117
116
  });
118
- endStream();
117
+ // handle stream messages
118
+ return this.#client.handleStreamMessage<SafeConsent[]>(
119
+ streamId,
120
+ callback,
121
+ {
122
+ ...options,
123
+ onFail,
124
+ },
125
+ );
119
126
  };
120
- return asyncStream;
127
+
128
+ return createStream(stream, undefined, options);
121
129
  }
122
130
 
123
131
  /**
124
132
  * Creates a stream of user preference updates
125
133
  *
126
- * @param callback - Optional callback function for handling stream updates
134
+ * @param options - Optional stream options
127
135
  * @returns Stream instance for preference updates
128
136
  */
129
- async streamPreferences(callback?: StreamCallback<UserPreference[]>) {
130
- const streamId = v4();
131
- const asyncStream = new AsyncStream<UserPreference[]>();
132
- const endStream = this.#client.handleStreamMessage<UserPreference[]>(
133
- streamId,
134
- (error, value) => {
135
- void asyncStream.callback(error, value ?? undefined);
136
- void callback?.(error, value ?? undefined);
137
- },
138
- );
139
- await this.#client.sendMessage("preferences.streamPreferences", {
140
- streamId,
141
- });
142
- asyncStream.onDone = () => {
143
- void this.#client.sendMessage("endStream", {
137
+ async streamPreferences(options?: StreamOptions<UserPreference[]>) {
138
+ const stream = async (
139
+ callback: StreamCallback<UserPreference[]>,
140
+ onFail: () => void,
141
+ ) => {
142
+ const streamId = v4();
143
+ // sync the conversation
144
+ await this.sync();
145
+ // start the stream
146
+ await this.#client.sendMessage("preferences.streamPreferences", {
144
147
  streamId,
145
148
  });
146
- endStream();
149
+ // handle stream messages
150
+ return this.#client.handleStreamMessage<UserPreference[]>(
151
+ streamId,
152
+ callback,
153
+ {
154
+ ...options,
155
+ onFail,
156
+ },
157
+ );
147
158
  };
148
- return asyncStream;
159
+
160
+ return createStream(stream, undefined, options);
149
161
  }
150
162
  }
@@ -12,12 +12,12 @@ import {
12
12
  type PermissionPolicy,
13
13
  type PermissionUpdateType,
14
14
  } from "@xmtp/wasm-bindings";
15
- import { type StreamCallback } from "@/AsyncStream";
16
15
  import {
17
16
  fromSafeListMessagesOptions,
18
17
  toSafeGroupMember,
19
18
  type SafeListMessagesOptions,
20
19
  } from "@/utils/conversions";
20
+ import type { StreamCallback } from "@/utils/streams";
21
21
  import type { WorkerClient } from "@/WorkerClient";
22
22
 
23
23
  export class WorkerConversation {
@@ -202,14 +202,17 @@ export class WorkerConversation {
202
202
  return this.#group.isMessageDisappearingEnabled();
203
203
  }
204
204
 
205
- stream(callback?: StreamCallback<Message>) {
205
+ stream(callback: StreamCallback<Message>, onFail: () => void) {
206
206
  const on_message = (message: Message) => {
207
- void callback?.(null, message);
207
+ callback(null, message);
208
208
  };
209
209
  const on_error = (error: Error | null) => {
210
- void callback?.(error, undefined);
210
+ callback(error, undefined);
211
211
  };
212
- return this.#group.stream({ on_message, on_error });
212
+ const on_close = () => {
213
+ onFail();
214
+ };
215
+ return this.#group.stream({ on_message, on_error, on_close });
213
216
  }
214
217
 
215
218
  pausedForVersion() {
@@ -7,7 +7,6 @@ import {
7
7
  type Identifier,
8
8
  type Message,
9
9
  } from "@xmtp/wasm-bindings";
10
- import type { StreamCallback } from "@/AsyncStream";
11
10
  import {
12
11
  fromSafeCreateDmOptions,
13
12
  fromSafeCreateGroupOptions,
@@ -17,6 +16,7 @@ import {
17
16
  type SafeCreateGroupOptions,
18
17
  type SafeListConversationsOptions,
19
18
  } from "@/utils/conversions";
19
+ import type { StreamCallback } from "@/utils/streams";
20
20
  import type { WorkerClient } from "@/WorkerClient";
21
21
  import { WorkerConversation } from "@/WorkerConversation";
22
22
 
@@ -151,42 +151,50 @@ export class WorkerConversations {
151
151
  }
152
152
 
153
153
  stream(
154
- callback?: StreamCallback<Conversation>,
154
+ callback: StreamCallback<Conversation>,
155
+ onFail: () => void,
155
156
  conversationType?: ConversationType,
156
157
  ) {
157
158
  const on_conversation = (conversation: Conversation) => {
158
- void callback?.(null, conversation);
159
+ callback(null, conversation);
159
160
  };
160
161
  const on_error = (error: Error | null) => {
161
- void callback?.(error, undefined);
162
+ callback(error, undefined);
163
+ };
164
+ const on_close = () => {
165
+ onFail();
162
166
  };
163
167
  return this.#conversations.stream(
164
- { on_conversation, on_error },
168
+ { on_conversation, on_error, on_close },
165
169
  conversationType,
166
170
  );
167
171
  }
168
172
 
169
- streamGroups(callback?: StreamCallback<Conversation>) {
170
- return this.#conversations.stream(callback, ConversationType.Group);
173
+ streamGroups(callback: StreamCallback<Conversation>, onFail: () => void) {
174
+ return this.stream(callback, onFail, ConversationType.Group);
171
175
  }
172
176
 
173
- streamDms(callback?: StreamCallback<Conversation>) {
174
- return this.#conversations.stream(callback, ConversationType.Dm);
177
+ streamDms(callback: StreamCallback<Conversation>, onFail: () => void) {
178
+ return this.stream(callback, onFail, ConversationType.Dm);
175
179
  }
176
180
 
177
181
  streamAllMessages(
178
- callback?: StreamCallback<Message>,
182
+ callback: StreamCallback<Message>,
183
+ onFail: () => void,
179
184
  conversationType?: ConversationType,
180
185
  consentStates?: ConsentState[],
181
186
  ) {
182
187
  const on_message = (message: Message) => {
183
- void callback?.(null, message);
188
+ callback(null, message);
184
189
  };
185
190
  const on_error = (error: Error | null) => {
186
- void callback?.(error, undefined);
191
+ callback(error, undefined);
192
+ };
193
+ const on_close = () => {
194
+ onFail();
187
195
  };
188
196
  return this.#conversations.streamAllMessages(
189
- { on_message, on_error },
197
+ { on_message, on_error, on_close },
190
198
  conversationType,
191
199
  consentStates,
192
200
  );