@remit/web-client 0.0.268 → 0.0.270

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.
@@ -15,6 +15,18 @@
15
15
  * failure (auth, the write, or the enqueue) that takes out the whole batch and
16
16
  * stops the run.
17
17
  *
18
+ * Cancellation is an `AbortSignal` the caller owns, handed to every call the run
19
+ * makes (#113). Aborting ends the request in flight rather than waiting for it,
20
+ * so at most the batch already on the wire is at stake instead of a whole page;
21
+ * the run then stops before the next request leaves. A rejection raised while
22
+ * the signal is aborted is that abort, so it reports `cancelled` rather than an
23
+ * error — the user asked for it.
24
+ *
25
+ * That classification reads `signal.aborted`, deliberately, and not the shape of
26
+ * the rejection (`isAbortError`): a genuine failure that raced the user's Stop
27
+ * should read as a stop. The user pressed it and the run is over either way, and
28
+ * an error banner for a run they ended themselves is the wrong answer.
29
+ *
18
30
  * Pure, framework-agnostic, and independently testable — no React, no fetch.
19
31
  * `useEscalatedActions.ts` supplies the real `ApplyBatch`/`FetchIdsPage`
20
32
  * implementations (the generated SDK client) and owns the React state.
@@ -22,6 +34,9 @@
22
34
 
23
35
  export const BULK_ACTION_CHUNK_SIZE = 100;
24
36
 
37
+ /** For a run with no Stop control to wire a signal to. */
38
+ const NEVER_ABORTED = new AbortController().signal;
39
+
25
40
  /**
26
41
  * Resolves a promise to a discriminated result instead of throwing, so a
27
42
  * caller can branch on infrastructure failure as a value — the app's
@@ -104,7 +119,10 @@ export interface BatchResult {
104
119
  }
105
120
 
106
121
  /** Applies the action to one batch of ids (≤100) with a single bulk call. */
107
- export type ApplyBatch = (ids: string[]) => Promise<BatchResult>;
122
+ export type ApplyBatch = (
123
+ ids: string[],
124
+ signal: AbortSignal,
125
+ ) => Promise<BatchResult>;
108
126
 
109
127
  export interface BulkActionProgress {
110
128
  /** Ids the action has been applied to so far. */
@@ -122,6 +140,9 @@ export interface BulkActionOutcome {
122
140
  * cancellation or a thrown error, the chunks the bounded run never attempted
123
141
  * (see `runChunkedAction`). Empty in the predicate case, which re-resolves on
124
142
  * every run rather than handing back a remainder (see `runPredicateAction`).
143
+ * An aborted batch is counted here too: aborting the request does not
144
+ * un-send what the server already accepted, so its ids are "may not have
145
+ * landed" rather than "did not", and every action here is idempotent.
125
146
  * There is no per-id failure source: a returned batch call counts every id in
126
147
  * it as accepted (see the module header).
127
148
  */
@@ -150,7 +171,7 @@ export const runChunkedAction = async (
150
171
  targets: readonly BulkActionTarget[],
151
172
  applyBatch: ApplyBatch,
152
173
  onProgress: (progress: BulkActionProgress) => void,
153
- isCancelled: () => boolean,
174
+ signal: AbortSignal,
154
175
  ): Promise<BulkActionOutcome> => {
155
176
  const chunks = chunkTargets(targets);
156
177
  const total = targets.length;
@@ -158,14 +179,15 @@ export const runChunkedAction = async (
158
179
  const failedIds: string[] = [];
159
180
 
160
181
  for (let i = 0; i < chunks.length; i++) {
161
- if (isCancelled()) {
182
+ if (signal.aborted) {
162
183
  failedIds.push(...chunks.slice(i).flat());
163
184
  return { done, failedIds, cancelled: true };
164
185
  }
165
186
  const chunk = chunks[i];
166
- const attempted = await attempt(applyBatch(chunk));
187
+ const attempted = await attempt(applyBatch(chunk, signal));
167
188
  if (!attempted.ok) {
168
189
  failedIds.push(...chunks.slice(i).flat());
190
+ if (signal.aborted) return { done, failedIds, cancelled: true };
169
191
  onProgress({ done, total });
170
192
  return { done, failedIds, cancelled: false, error: attempted.error };
171
193
  }
@@ -194,6 +216,10 @@ export const runChunkedAction = async (
194
216
  * focused row, or a selection the surface already scoped — so nothing here
195
217
  * carries an account to split by. A caller with a cross-account selection
196
218
  * belongs on `runChunkedAction`, whose targets name one (#872).
219
+ *
220
+ * These callers have no Stop control to wire a signal to — a mutation over a
221
+ * handful of ids has no progress surface to stop from — so the run is handed a
222
+ * signal that never aborts.
197
223
  */
198
224
  export const runChunkedMutation = async (
199
225
  ids: readonly string[],
@@ -206,7 +232,7 @@ export const runChunkedMutation = async (
206
232
  return { successCount: chunk.length, failureCount: 0 };
207
233
  },
208
234
  () => {},
209
- () => false,
235
+ NEVER_ABORTED,
210
236
  );
211
237
  };
212
238
 
@@ -218,6 +244,7 @@ export interface FetchIdsPageResult {
218
244
  /** Fetches one page of matching message ids for the active predicate. */
219
245
  export type FetchIdsPage = (
220
246
  continuationToken: string | undefined,
247
+ signal: AbortSignal,
221
248
  ) => Promise<FetchIdsPageResult>;
222
249
 
223
250
  /**
@@ -230,33 +257,36 @@ export type FetchIdsPage = (
230
257
  * `failedIds` for the unreached remainder, because those ids were never
231
258
  * fetched — there is nothing to hand back. A predicate resolves fresh on
232
259
  * every run (D2), so resuming is re-invoking this same function with the same
233
- * predicate.
260
+ * predicate. Aborting also ends the page or batch on the wire, so the run gives
261
+ * up at most the one request rather than the whole page it was mid-way through.
234
262
  */
235
263
  export const runPredicateAction = async (
236
264
  fetchIdsPage: FetchIdsPage,
237
265
  total: number,
238
266
  applyBatch: ApplyBatch,
239
267
  onProgress: (progress: BulkActionProgress) => void,
240
- isCancelled: () => boolean,
268
+ signal: AbortSignal,
241
269
  ): Promise<BulkActionOutcome> => {
242
270
  let done = 0;
243
271
  const failedIds: string[] = [];
244
272
  let token: string | undefined;
245
273
 
246
274
  do {
247
- if (isCancelled()) {
275
+ if (signal.aborted) {
248
276
  return { done, failedIds, cancelled: true };
249
277
  }
250
278
 
251
- const fetched = await attempt(fetchIdsPage(token));
279
+ const fetched = await attempt(fetchIdsPage(token, signal));
252
280
  if (!fetched.ok) {
281
+ if (signal.aborted) return { done, failedIds, cancelled: true };
253
282
  return { done, failedIds, cancelled: false, error: fetched.error };
254
283
  }
255
284
  const page = fetched.value;
256
285
 
257
286
  if (page.ids.length > 0) {
258
- const attempted = await attempt(applyBatch(page.ids));
287
+ const attempted = await attempt(applyBatch(page.ids, signal));
259
288
  if (!attempted.ok) {
289
+ if (signal.aborted) return { done, failedIds, cancelled: true };
260
290
  return { done, failedIds, cancelled: false, error: attempted.error };
261
291
  }
262
292
  done += page.ids.length;