@xmtp/browser-sdk 3.1.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,8 @@ import {
5
5
  type Conversations,
6
6
  type UserPreference,
7
7
  } from "@xmtp/wasm-bindings";
8
- import type { StreamCallback } from "@/AsyncStream";
9
8
  import { fromSafeConsent, type SafeConsent } from "@/utils/conversions";
9
+ import type { StreamCallback } from "@/utils/streams";
10
10
 
11
11
  export class WorkerPreferences {
12
12
  #client: Client;
@@ -47,26 +47,40 @@ export class WorkerPreferences {
47
47
  return this.#client.getConsentState(entityType, entity);
48
48
  }
49
49
 
50
- streamConsent(callback?: StreamCallback<Consent[]>) {
50
+ streamConsent(callback: StreamCallback<Consent[]>, onFail: () => void) {
51
51
  const on_consent_update = (consent: Consent[]) => {
52
- void callback?.(null, consent);
52
+ callback(null, consent);
53
53
  };
54
54
  const on_error = (error: Error | null) => {
55
- void callback?.(error, undefined);
55
+ callback(error, undefined);
56
56
  };
57
- return this.#conversations.streamConsent({ on_consent_update, on_error });
57
+ const on_close = () => {
58
+ onFail();
59
+ };
60
+ return this.#conversations.streamConsent({
61
+ on_consent_update,
62
+ on_error,
63
+ on_close,
64
+ });
58
65
  }
59
66
 
60
- streamPreferences(callback?: StreamCallback<UserPreference[]>) {
67
+ streamPreferences(
68
+ callback: StreamCallback<UserPreference[]>,
69
+ onFail: () => void,
70
+ ) {
61
71
  const on_user_preference_update = (preferences: UserPreference[]) => {
62
- void callback?.(null, preferences);
72
+ callback(null, preferences);
63
73
  };
64
74
  const on_error = (error: Error | null) => {
65
- void callback?.(error, undefined);
75
+ callback(error, undefined);
76
+ };
77
+ const on_close = () => {
78
+ onFail();
66
79
  };
67
80
  return this.#conversations.streamPreferences({
68
81
  on_user_preference_update,
69
82
  on_error,
83
+ on_close,
70
84
  });
71
85
  }
72
86
  }
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ export { Utils } from "./Utils";
10
10
  export { ApiUrls, HistorySyncUrls } from "./constants";
11
11
  export type * from "./types/options";
12
12
  export * from "./utils/conversions";
13
- export type { AsyncStream, StreamCallback } from "./AsyncStream";
13
+ export type { AsyncStreamProxy } from "./AsyncStream";
14
14
  export type {
15
15
  Identifier,
16
16
  IdentifierKind,
@@ -53,3 +53,4 @@ export {
53
53
  export type { Signer, SafeSigner, EOASigner, SCWSigner } from "./utils/signer";
54
54
  export { toSafeSigner } from "./utils/signer";
55
55
  export * from "./utils/errors";
56
+ export type * from "./utils/streams";
@@ -25,6 +25,11 @@ export type StreamAction =
25
25
  action: "stream.preferences";
26
26
  streamId: string;
27
27
  result: UserPreference[] | undefined;
28
+ }
29
+ | {
30
+ action: "stream.fail";
31
+ streamId: string;
32
+ result: undefined;
28
33
  };
29
34
 
30
35
  export type StreamActionName = StreamAction["action"];
@@ -59,3 +59,16 @@ export class MissingContentTypeError extends Error {
59
59
  super("Content type is required when sending content other than text");
60
60
  }
61
61
  }
62
+
63
+ export class StreamFailedError extends Error {
64
+ constructor(retryAttempts: number) {
65
+ const times = `time${retryAttempts !== 1 ? "s" : ""}`;
66
+ super(`Stream failed, retried ${retryAttempts} ${times}`);
67
+ }
68
+ }
69
+
70
+ export class StreamInvalidRetryAttemptsError extends Error {
71
+ constructor() {
72
+ super("Stream retry attempts must be greater than 0");
73
+ }
74
+ }
@@ -0,0 +1,210 @@
1
+ import { AsyncStream, createAsyncStreamProxy } from "@/AsyncStream";
2
+ import { StreamFailedError, StreamInvalidRetryAttemptsError } from "./errors";
3
+
4
+ const isPromise = <T = unknown>(value: unknown): value is Promise<T> => {
5
+ return (
6
+ !!value &&
7
+ (typeof value === "object" || typeof value === "function") &&
8
+ "then" in value &&
9
+ typeof value.then === "function"
10
+ );
11
+ };
12
+
13
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
14
+
15
+ export const DEFAULT_RETRY_DELAY = 10000; // milliseconds
16
+ export const DEFAULT_RETRY_ATTEMPTS = 6;
17
+
18
+ export type StreamOptions<T = unknown, V = T> = {
19
+ /**
20
+ * Called when the stream ends
21
+ */
22
+ onEnd?: () => void;
23
+ /**
24
+ * Called when a stream error occurs
25
+ */
26
+ onError?: (error: Error) => void;
27
+ /**
28
+ * Called when the stream fails
29
+ */
30
+ onFail?: () => void;
31
+ /**
32
+ * Called when the stream is restarted
33
+ */
34
+ onRestart?: () => void;
35
+ /**
36
+ * Called when the stream is retried
37
+ */
38
+ onRetry?: (attempts: number, maxAttempts: number) => void;
39
+ /**
40
+ * Called when a value is emitted from the stream
41
+ */
42
+ onValue?: (value: V) => void;
43
+ /**
44
+ * The number of times to retry the stream
45
+ * (default: 6)
46
+ */
47
+ retryAttempts?: number;
48
+ /**
49
+ * The delay between retries (in milliseconds)
50
+ * (default: 10000)
51
+ */
52
+ retryDelay?: number;
53
+ /**
54
+ * Whether to retry the stream if it fails
55
+ * (default: true)
56
+ */
57
+ retryOnFail?: boolean;
58
+ };
59
+
60
+ export type StreamCallback<T = unknown> = (
61
+ error: Error | null,
62
+ value: T | undefined,
63
+ ) => void;
64
+
65
+ export type StreamFunction<T = unknown> = (
66
+ callback: StreamCallback<T>,
67
+ onFail: () => void,
68
+ ) => Promise<() => void>;
69
+
70
+ export type StreamValueMutator<T = unknown, V = T> = (
71
+ value: T,
72
+ ) => V | Promise<V>;
73
+
74
+ /**
75
+ * Creates a stream from a stream function
76
+ *
77
+ * If the stream fails, an attempt will be made to restart it.
78
+ *
79
+ * This function is not intended to be used directly.
80
+ *
81
+ * @param streamFunction - The stream function to create a stream from
82
+ * @param streamValueMutator - An optional function to mutate the value emitted from the stream
83
+ * @param options - The options for the stream
84
+ * @param args - Additional arguments to pass to the stream function
85
+ * @returns An async iterable stream proxy
86
+ * @throws {StreamInvalidRetryAttemptsError} if the retryAttempts option is less than 0 and retryOnFail is true
87
+ * @throws {StreamFailedError} if the stream fails and can't be restarted
88
+ */
89
+ export const createStream = async <T = unknown, V = T>(
90
+ streamFunction: StreamFunction<T>,
91
+ streamValueMutator?: StreamValueMutator<T, V>,
92
+ options?: StreamOptions<T, V>,
93
+ ) => {
94
+ const {
95
+ onEnd,
96
+ onError,
97
+ onFail,
98
+ onRestart,
99
+ onRetry,
100
+ onValue,
101
+ retryAttempts = DEFAULT_RETRY_ATTEMPTS,
102
+ retryDelay = DEFAULT_RETRY_DELAY,
103
+ retryOnFail = true,
104
+ } = options ?? {};
105
+ // retry attempts must be greater than 0
106
+ if (retryOnFail && retryAttempts < 0) {
107
+ throw new StreamInvalidRetryAttemptsError();
108
+ }
109
+
110
+ const asyncStream = new AsyncStream<V>();
111
+ const streamCallback: StreamCallback<T> = (error, value) => {
112
+ // if a stream error occurs, call the onError callback
113
+ if (error) {
114
+ onError?.(error);
115
+ return;
116
+ }
117
+ // ensure the value is not undefined
118
+ if (value !== undefined) {
119
+ try {
120
+ // if a streamValueMutator is provided, mutate the value
121
+ if (streamValueMutator) {
122
+ const mutatedValue = streamValueMutator(value);
123
+ if (isPromise(mutatedValue)) {
124
+ void mutatedValue
125
+ .then((mutatedValue) => {
126
+ asyncStream.push(mutatedValue);
127
+ onValue?.(mutatedValue);
128
+ })
129
+ .catch((error: unknown) => {
130
+ onError?.(error as Error);
131
+ });
132
+ } else {
133
+ asyncStream.push(mutatedValue);
134
+ onValue?.(mutatedValue);
135
+ }
136
+ } else {
137
+ asyncStream.push(value as unknown as V);
138
+ onValue?.(value as unknown as V);
139
+ }
140
+ } catch (error) {
141
+ onError?.(error as Error);
142
+ }
143
+ }
144
+ };
145
+ const retry = async (retries: number = retryAttempts) => {
146
+ try {
147
+ // if the stream has been retried the maximum number of times without
148
+ // success, throw an error
149
+ if (retries === 0) {
150
+ void asyncStream.end();
151
+ throw new StreamFailedError(retryAttempts);
152
+ }
153
+
154
+ // wait for the retry delay before attempting to restart the stream
155
+ await wait(retryDelay);
156
+ // call the onRetry callback
157
+ onRetry?.(retryAttempts - retries + 1, retryAttempts);
158
+
159
+ // attempt to restart the stream
160
+ const streamCloser = await streamFunction(streamCallback, () => {
161
+ // call the onFail callback
162
+ onFail?.();
163
+ void retry();
164
+ });
165
+
166
+ // when the async stream is done, end the stream
167
+ asyncStream.onDone = () => {
168
+ streamCloser();
169
+ onEnd?.();
170
+ };
171
+
172
+ // stream restarted, call the onRestart callback
173
+ onRestart?.();
174
+ } catch (error) {
175
+ onError?.(error as Error);
176
+ // retry
177
+ void retry(retries - 1);
178
+ }
179
+ };
180
+ const startRetry = () => {
181
+ // if the stream should be retried, start the process
182
+ if (retryOnFail) {
183
+ void retry();
184
+ } else {
185
+ void asyncStream.end();
186
+ // stream failed and should not be retried, throw an error
187
+ throw new StreamFailedError(0);
188
+ }
189
+ };
190
+
191
+ try {
192
+ // create the stream
193
+ const streamCloser = await streamFunction(streamCallback, () => {
194
+ // call the onFail callback
195
+ onFail?.();
196
+ startRetry();
197
+ });
198
+ // when the async stream is done, end the stream
199
+ asyncStream.onDone = () => {
200
+ streamCloser();
201
+ onEnd?.();
202
+ };
203
+ } catch (error) {
204
+ onError?.(error as Error);
205
+ startRetry();
206
+ }
207
+
208
+ // return a proxy for the async stream
209
+ return createAsyncStreamProxy(asyncStream);
210
+ };
@@ -448,7 +448,17 @@ self.onmessage = async (
448
448
  });
449
449
  }
450
450
  };
451
- const streamCloser = client.preferences.streamConsent(streamCallback);
451
+ const streamCloser = client.preferences.streamConsent(
452
+ streamCallback,
453
+ () => {
454
+ streamClosers.delete(data.streamId);
455
+ postStreamMessage({
456
+ action: "stream.fail",
457
+ streamId: data.streamId,
458
+ result: undefined,
459
+ });
460
+ },
461
+ );
452
462
  streamClosers.set(data.streamId, streamCloser);
453
463
  postMessage({
454
464
  id,
@@ -476,8 +486,17 @@ self.onmessage = async (
476
486
  });
477
487
  }
478
488
  };
479
- const streamCloser =
480
- client.preferences.streamPreferences(streamCallback);
489
+ const streamCloser = client.preferences.streamPreferences(
490
+ streamCallback,
491
+ () => {
492
+ streamClosers.delete(data.streamId);
493
+ postStreamMessage({
494
+ action: "stream.fail",
495
+ streamId: data.streamId,
496
+ result: undefined,
497
+ });
498
+ },
499
+ );
481
500
  streamClosers.set(data.streamId, streamCloser);
482
501
  postMessage({
483
502
  id,
@@ -490,7 +509,7 @@ self.onmessage = async (
490
509
  * Conversations actions
491
510
  */
492
511
  case "conversations.stream": {
493
- const streamCallback = async (
512
+ const streamCallback = (
494
513
  error: Error | null,
495
514
  value: Conversation | undefined,
496
515
  ) => {
@@ -501,19 +520,41 @@ self.onmessage = async (
501
520
  error,
502
521
  });
503
522
  } else {
504
- postStreamMessage({
505
- action: "stream.conversation",
506
- streamId: data.streamId,
507
- result: value
508
- ? await toSafeConversation(
509
- new WorkerConversation(client, value),
510
- )
511
- : undefined,
512
- });
523
+ if (value) {
524
+ toSafeConversation(new WorkerConversation(client, value))
525
+ .then((result) => {
526
+ postStreamMessage({
527
+ action: "stream.conversation",
528
+ streamId: data.streamId,
529
+ result,
530
+ });
531
+ })
532
+ .catch((error: unknown) => {
533
+ postStreamMessageError({
534
+ action: "stream.conversation",
535
+ streamId: data.streamId,
536
+ error: error as Error,
537
+ });
538
+ });
539
+ } else {
540
+ postStreamMessage({
541
+ action: "stream.conversation",
542
+ streamId: data.streamId,
543
+ result: undefined,
544
+ });
545
+ }
513
546
  }
514
547
  };
515
548
  const streamCloser = client.conversations.stream(
516
549
  streamCallback,
550
+ () => {
551
+ streamClosers.delete(data.streamId);
552
+ postStreamMessage({
553
+ action: "stream.fail",
554
+ streamId: data.streamId,
555
+ result: undefined,
556
+ });
557
+ },
517
558
  data.conversationType,
518
559
  );
519
560
  streamClosers.set(data.streamId, streamCloser);
@@ -541,6 +582,14 @@ self.onmessage = async (
541
582
  };
542
583
  const streamCloser = client.conversations.streamAllMessages(
543
584
  streamCallback,
585
+ () => {
586
+ streamClosers.delete(data.streamId);
587
+ postStreamMessage({
588
+ action: "stream.fail",
589
+ streamId: data.streamId,
590
+ result: undefined,
591
+ });
592
+ },
544
593
  data.conversationType,
545
594
  data.consentStates,
546
595
  );
@@ -876,7 +925,14 @@ self.onmessage = async (
876
925
  });
877
926
  }
878
927
  };
879
- const streamCloser = group.stream(streamCallback);
928
+ const streamCloser = group.stream(streamCallback, () => {
929
+ streamClosers.delete(data.streamId);
930
+ postStreamMessage({
931
+ action: "stream.fail",
932
+ streamId: data.streamId,
933
+ result: undefined,
934
+ });
935
+ });
880
936
  streamClosers.set(data.streamId, streamCloser);
881
937
  postMessage({ id, action, result: undefined });
882
938
  break;