@remit/web-client 0.0.118 → 0.0.119
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/components/mail/SelectionWizardHost.tsx +4 -1
- package/src/components/mail/organize-run-state.test.ts +18 -0
- package/src/components/mail/organize-run-state.ts +5 -0
- package/src/hooks/useOrganizeJob.render.test.ts +52 -4
- package/src/hooks/useOrganizeJob.ts +23 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.119",
|
|
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": {
|
|
@@ -719,7 +719,10 @@ function SelectionWizardSession({
|
|
|
719
719
|
state,
|
|
720
720
|
matched: progress.matchedCount,
|
|
721
721
|
applied: progress.appliedCount,
|
|
722
|
-
failed:
|
|
722
|
+
failed:
|
|
723
|
+
state === "backApplyFailed" || state === "backApplyRestartFailed"
|
|
724
|
+
? progress.failedCount
|
|
725
|
+
: 0,
|
|
723
726
|
failures: [],
|
|
724
727
|
};
|
|
725
728
|
},
|
|
@@ -59,6 +59,24 @@ describe("organizeRunState", () => {
|
|
|
59
59
|
);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
it("keeps a finished pass's ending when the retry over it could not be started", () => {
|
|
63
|
+
// #552: the retry is a second create, and a create that failed over a pass
|
|
64
|
+
// that already moved mail is not a pass that never ran.
|
|
65
|
+
for (const ruleSaved of [true, false]) {
|
|
66
|
+
assert.equal(
|
|
67
|
+
organizeRunState(
|
|
68
|
+
reading({
|
|
69
|
+
ruleSaved,
|
|
70
|
+
isDone: true,
|
|
71
|
+
failedCount: 84,
|
|
72
|
+
failure: { kind: "restartFailed", error: new Error("offline") },
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
"backApplyRestartFailed",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
62
80
|
it("says nothing happened only when the create itself failed", () => {
|
|
63
81
|
assert.equal(
|
|
64
82
|
organizeRunState(
|
|
@@ -20,6 +20,10 @@ export interface OrganizeJobReading {
|
|
|
20
20
|
* not a job that never started (#526), so what the job is doing is read before
|
|
21
21
|
* what failed: a dropped poll leaves a running pass running and a finished one
|
|
22
22
|
* finished, and only a create that never returned an id says nothing happened.
|
|
23
|
+
*
|
|
24
|
+
* A create that failed over a pass that already ran is that same distinction on
|
|
25
|
+
* the create path (#552): the counts of the pass that ran stand, and what failed
|
|
26
|
+
* is the retry.
|
|
23
27
|
*/
|
|
24
28
|
export const organizeRunState = ({
|
|
25
29
|
failure,
|
|
@@ -29,6 +33,7 @@ export const organizeRunState = ({
|
|
|
29
33
|
failedCount,
|
|
30
34
|
ruleSaved,
|
|
31
35
|
}: OrganizeJobReading): RunState => {
|
|
36
|
+
if (failure?.kind === "restartFailed") return "backApplyRestartFailed";
|
|
32
37
|
if (failure?.kind === "startFailed") {
|
|
33
38
|
return ruleSaved ? "backApplyStartFailed" : "commitFailed";
|
|
34
39
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useOrganizeJob — the back-apply job seam. It reports
|
|
3
|
-
* the same fact (#526): a create that never returned a job id,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* useOrganizeJob — the back-apply job seam. It reports three failures that are
|
|
3
|
+
* not the same fact (#526, #552): a create that never returned a job id, that
|
|
4
|
+
* same create over a pass that already ran, and a status poll that could not be
|
|
5
|
+
* read over a job the server is already running. Looking at that job again is a
|
|
6
|
+
* separate move from starting one.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import assert from "node:assert/strict";
|
|
@@ -75,6 +76,36 @@ const startJob = async (status: () => unknown): Promise<void> => {
|
|
|
75
76
|
await settle();
|
|
76
77
|
};
|
|
77
78
|
|
|
79
|
+
const COMPLETED_PASS = {
|
|
80
|
+
organizeJobId: JOB,
|
|
81
|
+
state: "Complete",
|
|
82
|
+
matchedCount: 1284,
|
|
83
|
+
appliedCount: 1200,
|
|
84
|
+
failedCount: 84,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Run one pass to a finish, then answer the next create with `restart`. */
|
|
88
|
+
const restartAfterPass = async (restart: () => unknown): Promise<void> => {
|
|
89
|
+
let created = false;
|
|
90
|
+
http = mockFetch((call) => {
|
|
91
|
+
if (call.method !== "POST") return COMPLETED_PASS;
|
|
92
|
+
if (created) return restart();
|
|
93
|
+
created = true;
|
|
94
|
+
return { organizeJobId: JOB, state: "Pending" };
|
|
95
|
+
});
|
|
96
|
+
harness = createDomHarness();
|
|
97
|
+
harness.renderApp(createElement(Probe));
|
|
98
|
+
await act(async () => {
|
|
99
|
+
current().start(DRAFT);
|
|
100
|
+
});
|
|
101
|
+
await settle();
|
|
102
|
+
assert.equal(current().isDone, true, "the first pass never finished");
|
|
103
|
+
await act(async () => {
|
|
104
|
+
current().start(DRAFT);
|
|
105
|
+
});
|
|
106
|
+
await settle();
|
|
107
|
+
};
|
|
108
|
+
|
|
78
109
|
const posts = (): number =>
|
|
79
110
|
(http?.calls ?? []).filter((call) => call.method === "POST").length;
|
|
80
111
|
|
|
@@ -128,6 +159,23 @@ describe("useOrganizeJob status reporting", () => {
|
|
|
128
159
|
assert.equal(current().progress.matchedCount, 1284);
|
|
129
160
|
});
|
|
130
161
|
|
|
162
|
+
it("reads a create that failed over a finished pass as a restart, with that pass's counts", async () => {
|
|
163
|
+
await restartAfterPass(dropped);
|
|
164
|
+
assert.equal(current().failure?.kind, "restartFailed");
|
|
165
|
+
assert.equal(current().progress.matchedCount, 1284);
|
|
166
|
+
assert.equal(current().progress.appliedCount, 1200);
|
|
167
|
+
assert.equal(current().progress.failedCount, 84);
|
|
168
|
+
assert.equal(current().isDone, true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("reports a restart that is under way as its own pass, not the one before it", async () => {
|
|
172
|
+
await restartAfterPass(() => new Promise<never>(() => {}));
|
|
173
|
+
assert.equal(current().isStarting, true);
|
|
174
|
+
assert.equal(current().isDone, false);
|
|
175
|
+
assert.equal(current().failure, undefined);
|
|
176
|
+
assert.equal(current().progress.matchedCount, 0);
|
|
177
|
+
});
|
|
178
|
+
|
|
131
179
|
it("stops reporting a job as running once it reaches a terminal state", async () => {
|
|
132
180
|
await startJob(() => ({
|
|
133
181
|
organizeJobId: JOB,
|
|
@@ -23,20 +23,28 @@ export interface OrganizeJobProgress {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Why the job is not reporting, which is
|
|
27
|
-
* that never returned an id means nothing was started;
|
|
28
|
-
*
|
|
26
|
+
* Why the job is not reporting, which is three separate facts (#526, #552). A
|
|
27
|
+
* create that never returned an id means nothing was started; the same create
|
|
28
|
+
* over a pass that already ran means that pass stands and only the retry never
|
|
29
|
+
* left; a status read that failed means a job is out there and this client
|
|
30
|
+
* cannot see how far it got.
|
|
29
31
|
*/
|
|
30
32
|
export interface OrganizeJobFailure {
|
|
31
|
-
kind: "startFailed" | "statusUnreadable";
|
|
33
|
+
kind: "startFailed" | "restartFailed" | "statusUnreadable";
|
|
32
34
|
error: unknown;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const organizeJobFailure = (
|
|
36
38
|
createError: unknown,
|
|
37
39
|
statusError: unknown,
|
|
40
|
+
passAlreadyRun: boolean,
|
|
38
41
|
): OrganizeJobFailure | undefined => {
|
|
39
|
-
if (createError)
|
|
42
|
+
if (createError) {
|
|
43
|
+
return {
|
|
44
|
+
kind: passAlreadyRun ? "restartFailed" : "startFailed",
|
|
45
|
+
error: createError,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
40
48
|
if (statusError) return { kind: "statusUnreadable", error: statusError };
|
|
41
49
|
return undefined;
|
|
42
50
|
};
|
|
@@ -76,7 +84,6 @@ export const useOrganizeJob = (accountId: string | undefined) => {
|
|
|
76
84
|
const start = useCallback(
|
|
77
85
|
(draft: OrganizeDraft) => {
|
|
78
86
|
if (!accountId) return;
|
|
79
|
-
setOrganizeJobId(undefined);
|
|
80
87
|
createJob({
|
|
81
88
|
path: { accountId },
|
|
82
89
|
body: buildOrganizeInput(draft),
|
|
@@ -92,7 +99,11 @@ export const useOrganizeJob = (accountId: string | undefined) => {
|
|
|
92
99
|
void refetch();
|
|
93
100
|
}, [refetch]);
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
// The last pass this client read. A restart replaces it only once the server
|
|
103
|
+
// hands back a job id: while the create is in flight those counts are not this
|
|
104
|
+
// pass's, and a create that fails leaves them standing (#552).
|
|
105
|
+
const lastPass = jobQuery.data;
|
|
106
|
+
const job = createMutation.isPending ? undefined : lastPass;
|
|
96
107
|
const state = job?.state ?? createMutation.data?.state;
|
|
97
108
|
const isDone = isTerminalJobState(job?.state);
|
|
98
109
|
|
|
@@ -111,6 +122,10 @@ export const useOrganizeJob = (accountId: string | undefined) => {
|
|
|
111
122
|
isStarting: createMutation.isPending,
|
|
112
123
|
isRunning: !!organizeJobId && !isDone,
|
|
113
124
|
isDone,
|
|
114
|
-
failure: organizeJobFailure(
|
|
125
|
+
failure: organizeJobFailure(
|
|
126
|
+
createMutation.error,
|
|
127
|
+
jobQuery.error,
|
|
128
|
+
!!lastPass,
|
|
129
|
+
),
|
|
115
130
|
};
|
|
116
131
|
};
|