stream-chat 9.41.1 → 9.42.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.
@@ -0,0 +1,176 @@
1
+ import type { StreamChat } from './client';
2
+ import type { UploadRequestOptions } from './messageComposer/configuration/types';
3
+ import { StateStore } from './store';
4
+ import type { AttachmentManager } from '.';
5
+
6
+ export type UploadRecord = {
7
+ id: string;
8
+ uploadProgress?: number;
9
+ };
10
+
11
+ export type UploadManagerState = {
12
+ uploads: Record<string, UploadRecord>;
13
+ };
14
+
15
+ const initState = (): UploadManagerState => ({ uploads: {} });
16
+
17
+ const upsertById = (
18
+ uploads: Record<string, UploadRecord>,
19
+ record: UploadRecord,
20
+ ): Record<string, UploadRecord> => ({
21
+ ...uploads,
22
+ [record.id]: { ...uploads[record.id], ...record },
23
+ });
24
+
25
+ const updateById = (
26
+ uploads: Record<string, UploadRecord>,
27
+ record: UploadRecord,
28
+ ): Record<string, UploadRecord> | null => {
29
+ if (!(record.id in uploads)) return null;
30
+ const current = uploads[record.id];
31
+ return { ...uploads, [record.id]: { ...current, ...record } };
32
+ };
33
+
34
+ type UploadPromise = ReturnType<typeof AttachmentManager.prototype.doUploadRequest>;
35
+
36
+ type InFlightUpload = { promise: UploadPromise; abortController: AbortController };
37
+
38
+ /**
39
+ * @internal
40
+ */
41
+ export class UploadManager {
42
+ readonly state: StateStore<UploadManagerState>;
43
+
44
+ private inFlightUploads = new Map<string, InFlightUpload>();
45
+
46
+ constructor(private readonly client: StreamChat) {
47
+ this.state = new StateStore<UploadManagerState>(initState());
48
+ }
49
+
50
+ private resolveAttachmentManager(channelCid: string) {
51
+ const colon = channelCid.indexOf(':');
52
+ if (colon <= 0 || colon === channelCid.length - 1) {
53
+ throw new Error(`Invalid channelCid: ${channelCid}`);
54
+ }
55
+ const channelType = channelCid.slice(0, colon);
56
+ const channelId = channelCid.slice(colon + 1);
57
+ return this.client.channel(channelType, channelId).messageComposer.attachmentManager;
58
+ }
59
+
60
+ get uploads() {
61
+ return this.state.getLatestValue().uploads;
62
+ }
63
+
64
+ getUpload = (id: string) => this.uploads[id];
65
+
66
+ /**
67
+ * Clears all upload records.
68
+ * Invoked when the user disconnects so a later session does not inherit stale upload state.
69
+ * Aborts every in-flight upload request via its `UploadRequestOptions.abortSignal`.
70
+ */
71
+ reset = () => {
72
+ for (const { abortController } of this.inFlightUploads.values()) {
73
+ abortController.abort();
74
+ }
75
+ this.inFlightUploads.clear();
76
+ this.state.next(initState());
77
+ };
78
+
79
+ /**
80
+ * Removes the upload record for `id` if present.
81
+ * If an upload is still in progress, aborts its `UploadRequestOptions.abortSignal`.
82
+ */
83
+ deleteUploadRecord = (id: string) => {
84
+ const flight = this.inFlightUploads.get(id);
85
+ if (flight) {
86
+ this.inFlightUploads.delete(id);
87
+ flight.abortController.abort();
88
+ }
89
+ this.state.next((current) => {
90
+ if (!(id in current.uploads)) return current;
91
+ const uploads = { ...current.uploads };
92
+ delete uploads[id];
93
+ return { ...current, uploads };
94
+ });
95
+ };
96
+
97
+ /**
98
+ * Starts an upload for `id`, or returns the existing in-flight promise if one is already running.
99
+ * Uses {@link StreamChat.channel}(`channelCid`) → `messageComposer.attachmentManager.doUploadRequest`.
100
+ * Resolves with that result; rejects if the upload rejects (the record is removed from state either way).
101
+ */
102
+ upload = ({
103
+ id,
104
+ channelCid,
105
+ file,
106
+ }: {
107
+ id: string;
108
+ channelCid: string;
109
+ file: Parameters<typeof AttachmentManager.prototype.doUploadRequest>[0];
110
+ }): ReturnType<typeof AttachmentManager.prototype.doUploadRequest> => {
111
+ const existing = this.inFlightUploads.get(id);
112
+ if (existing) return existing.promise;
113
+
114
+ let resolvePromise!: (value: Awaited<UploadPromise>) => void;
115
+ let rejectPromise!: (reason?: unknown) => void;
116
+ const promise = new Promise<Awaited<UploadPromise>>((resolve, reject) => {
117
+ resolvePromise = resolve;
118
+ rejectPromise = reject;
119
+ });
120
+
121
+ const abortController = new AbortController();
122
+ this.inFlightUploads.set(id, { promise, abortController });
123
+
124
+ void (async () => {
125
+ const attachmentManager = this.resolveAttachmentManager(channelCid);
126
+ const trackProgress = attachmentManager.config.trackUploadProgress;
127
+ try {
128
+ this.upsertUpload({
129
+ id,
130
+ uploadProgress: trackProgress ? 0 : undefined,
131
+ });
132
+
133
+ const onProgress = trackProgress
134
+ ? (progress?: number) => {
135
+ this.updateUpload({
136
+ id,
137
+ uploadProgress: progress,
138
+ });
139
+ }
140
+ : undefined;
141
+
142
+ const uploadRequestOptions: UploadRequestOptions = {
143
+ abortSignal: abortController.signal,
144
+ ...(onProgress ? { onProgress } : {}),
145
+ };
146
+
147
+ const response = await attachmentManager.doUploadRequest(
148
+ file,
149
+ uploadRequestOptions,
150
+ );
151
+ resolvePromise(response);
152
+ } catch (error) {
153
+ rejectPromise(error);
154
+ } finally {
155
+ this.inFlightUploads.delete(id);
156
+ this.deleteUploadRecord(id);
157
+ }
158
+ })();
159
+
160
+ return promise;
161
+ };
162
+
163
+ private upsertUpload = (record: UploadRecord) => {
164
+ this.state.partialNext({
165
+ uploads: upsertById(this.uploads, record),
166
+ });
167
+ };
168
+
169
+ private updateUpload = (record: UploadRecord) => {
170
+ this.state.next((current) => {
171
+ const nextUploads = updateById(current.uploads, record);
172
+ if (!nextUploads) return current;
173
+ return { ...current, uploads: nextUploads };
174
+ });
175
+ };
176
+ }