@remit/web-client 0.0.34 → 0.0.35
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/package.json +1 -1
- package/src/lib/bulk-delete.test.ts +6 -48
- package/src/lib/bulk-delete.ts +22 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
BULK_DELETE_CHUNK_SIZE,
|
|
5
5
|
chunkIds,
|
|
6
6
|
countMatches,
|
|
7
|
-
type DeleteBatchResult,
|
|
8
7
|
type FetchIdsPageResult,
|
|
9
8
|
honestProgress,
|
|
10
9
|
resolveSelectionAfterDelete,
|
|
@@ -90,36 +89,16 @@ describe("runChunkedDelete", () => {
|
|
|
90
89
|
assert.deepEqual(outcome.failedIds, []);
|
|
91
90
|
});
|
|
92
91
|
|
|
93
|
-
test("
|
|
92
|
+
test("a returned batch counts every id in it as accepted", async () => {
|
|
94
93
|
const input = ids(5);
|
|
95
94
|
const outcome = await runChunkedDelete(
|
|
96
95
|
input,
|
|
97
|
-
async (chunk) => ({
|
|
98
|
-
successCount: chunk.length - 2,
|
|
99
|
-
failureCount: 2,
|
|
100
|
-
failedIds: chunk.slice(0, 2),
|
|
101
|
-
}),
|
|
102
|
-
noopProgress,
|
|
103
|
-
neverCancelled,
|
|
104
|
-
);
|
|
105
|
-
assert.equal(outcome.done, 3);
|
|
106
|
-
assert.deepEqual(outcome.failedIds, input.slice(0, 2));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("partial failure: failed ids are reported, not rolled into done", async () => {
|
|
110
|
-
const input = ids(3);
|
|
111
|
-
const outcome = await runChunkedDelete(
|
|
112
|
-
input,
|
|
113
|
-
async (chunk) => ({
|
|
114
|
-
successCount: 2,
|
|
115
|
-
failureCount: 1,
|
|
116
|
-
failedIds: [chunk[1]],
|
|
117
|
-
}),
|
|
96
|
+
async (chunk) => ({ successCount: chunk.length, failureCount: 0 }),
|
|
118
97
|
noopProgress,
|
|
119
98
|
neverCancelled,
|
|
120
99
|
);
|
|
121
|
-
assert.equal(outcome.done,
|
|
122
|
-
assert.deepEqual(outcome.failedIds, [
|
|
100
|
+
assert.equal(outcome.done, input.length);
|
|
101
|
+
assert.deepEqual(outcome.failedIds, []);
|
|
123
102
|
});
|
|
124
103
|
|
|
125
104
|
test("cancelling mid-run folds every unreached chunk into failedIds", async () => {
|
|
@@ -264,27 +243,6 @@ describe("runPredicateDelete", () => {
|
|
|
264
243
|
assert.equal(outcome.done, 4);
|
|
265
244
|
});
|
|
266
245
|
|
|
267
|
-
test("partial failure across pages accumulates failedIds without rolling into done", async () => {
|
|
268
|
-
const fetch = pagedFetcher([
|
|
269
|
-
{ ids: ["a", "b"], continuationToken: "t1" },
|
|
270
|
-
{ ids: ["c"] },
|
|
271
|
-
]);
|
|
272
|
-
const outcome = await runPredicateDelete(
|
|
273
|
-
fetch,
|
|
274
|
-
3,
|
|
275
|
-
async (chunk): Promise<DeleteBatchResult> => {
|
|
276
|
-
if (chunk.includes("b")) {
|
|
277
|
-
return { successCount: 1, failureCount: 1, failedIds: ["b"] };
|
|
278
|
-
}
|
|
279
|
-
return { successCount: chunk.length, failureCount: 0 };
|
|
280
|
-
},
|
|
281
|
-
noopProgress,
|
|
282
|
-
neverCancelled,
|
|
283
|
-
);
|
|
284
|
-
assert.equal(outcome.done, 2);
|
|
285
|
-
assert.deepEqual(outcome.failedIds, ["b"]);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
246
|
test("cancelling mid-delete stops paging without inventing failedIds for unfetched pages", async () => {
|
|
289
247
|
let fetchCalls = 0;
|
|
290
248
|
let cancelled = false;
|
|
@@ -409,7 +367,7 @@ describe("resolveSelectionAfterDelete", () => {
|
|
|
409
367
|
);
|
|
410
368
|
});
|
|
411
369
|
|
|
412
|
-
test("
|
|
370
|
+
test("unreached ids stay selected for a precise retry, even alongside a clean stop", () => {
|
|
413
371
|
assert.deepEqual(
|
|
414
372
|
resolveSelectionAfterDelete({
|
|
415
373
|
done: 3072,
|
|
@@ -431,7 +389,7 @@ describe("resolveSelectionAfterDelete", () => {
|
|
|
431
389
|
);
|
|
432
390
|
});
|
|
433
391
|
|
|
434
|
-
test("an infra failure with
|
|
392
|
+
test("an infra failure with nothing left unreached still keeps selection mode open", () => {
|
|
435
393
|
assert.deepEqual(
|
|
436
394
|
resolveSelectionAfterDelete({
|
|
437
395
|
done: 0,
|
package/src/lib/bulk-delete.ts
CHANGED
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The bulk endpoint caps a call at 100 ids (`BulkMessageInput.messageIds`,
|
|
5
5
|
* `@maxItems(100)`); a delete over a search result can run to thousands. These
|
|
6
|
-
* helpers sequence the calls and tally
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* helpers sequence the calls and tally which ids the run actually reached, so a
|
|
7
|
+
* caller can always ask "what's still not deleted" instead of trusting the
|
|
8
|
+
* request it sent.
|
|
9
|
+
*
|
|
10
|
+
* The endpoint enqueues the IMAP delete and returns; it does not apply it. So a
|
|
11
|
+
* returned call means every id in it was accepted for deletion, not that the
|
|
12
|
+
* mail server removed it — there is no per-id success/failure in the response
|
|
13
|
+
* to read. The only failure this layer can observe is a thrown call: an
|
|
14
|
+
* infrastructure failure (auth, the write, or the enqueue) that takes out the
|
|
15
|
+
* whole batch and stops the run.
|
|
9
16
|
*
|
|
10
17
|
* Pure, framework-agnostic, and independently testable — no React, no fetch.
|
|
11
18
|
* `useEscalatedDelete.ts` supplies the real `DeleteBatch`/`FetchIdsPage`
|
|
@@ -46,7 +53,6 @@ export const chunkIds = (
|
|
|
46
53
|
export interface DeleteBatchResult {
|
|
47
54
|
successCount: number;
|
|
48
55
|
failureCount: number;
|
|
49
|
-
failedIds?: string[];
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
/** Sends one bulk-delete call for the given ids (≤100). */
|
|
@@ -64,9 +70,12 @@ export interface BulkDeleteOutcome {
|
|
|
64
70
|
/** Ids confirmed deleted (server-reported success, not merely "sent"). */
|
|
65
71
|
done: number;
|
|
66
72
|
/**
|
|
67
|
-
* Ids not
|
|
68
|
-
*
|
|
69
|
-
*
|
|
73
|
+
* Ids the run did not reach and are therefore not confirmed deleted: on
|
|
74
|
+
* cancellation or a thrown error, the chunks the bounded run never attempted
|
|
75
|
+
* (see `runChunkedDelete`). Empty in the predicate case, which re-resolves on
|
|
76
|
+
* every run rather than handing back a remainder (see `runPredicateDelete`).
|
|
77
|
+
* There is no per-id failure source: a returned batch call counts every id in
|
|
78
|
+
* it as accepted (see the module header).
|
|
70
79
|
*/
|
|
71
80
|
failedIds: string[];
|
|
72
81
|
cancelled: boolean;
|
|
@@ -106,9 +115,7 @@ export const runChunkedDelete = async (
|
|
|
106
115
|
onProgress({ done, total });
|
|
107
116
|
return { done, failedIds, cancelled: false, error: attempted.error };
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
done += chunk.length - failedInChunk.size;
|
|
111
|
-
failedIds.push(...chunk.filter((id) => failedInChunk.has(id)));
|
|
118
|
+
done += chunk.length;
|
|
112
119
|
onProgress({ done, total });
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -165,9 +172,7 @@ export const runPredicateDelete = async (
|
|
|
165
172
|
if (!attempted.ok) {
|
|
166
173
|
return { done, failedIds, cancelled: false, error: attempted.error };
|
|
167
174
|
}
|
|
168
|
-
|
|
169
|
-
done += page.ids.length - failedInPage.size;
|
|
170
|
-
failedIds.push(...page.ids.filter((id) => failedInPage.has(id)));
|
|
175
|
+
done += page.ids.length;
|
|
171
176
|
onProgress({ done, total });
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -250,11 +255,10 @@ export interface SelectionAfterDelete {
|
|
|
250
255
|
|
|
251
256
|
/**
|
|
252
257
|
* What a caller does with selection once a run ends, for any reason. Every id
|
|
253
|
-
* not confirmed deleted —
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* requirement 8).
|
|
258
|
+
* not confirmed deleted — a chunk the bounded run never reached because it was
|
|
259
|
+
* stopped or errored — belongs in `retryIds`: it is exactly what Retry should
|
|
260
|
+
* resend, and it is what stays selected so the count on screen never claims
|
|
261
|
+
* more was deleted than actually was (#92 requirement 8).
|
|
258
262
|
*/
|
|
259
263
|
export const resolveSelectionAfterDelete = (
|
|
260
264
|
outcome: DeleteRunOutcome,
|