@remit/web-client 0.0.106 → 0.0.107
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.job-status.test.ts +50 -0
- package/src/components/mail/SelectionWizardHost.tsx +38 -27
- package/src/components/mail/organize-run-state.test.ts +97 -0
- package/src/components/mail/organize-run-state.ts +39 -0
- package/src/hooks/useOrganizeJob.render.test.ts +142 -0
- package/src/hooks/useOrganizeJob.ts +28 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.107",
|
|
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": {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A back-apply whose status could not be read is still running on the server
|
|
9
|
+
* (#526), so the run screen's affordance looks at that job again rather than
|
|
10
|
+
* queuing a second pass over the same mail.
|
|
11
|
+
*
|
|
12
|
+
* The host wires routing, history and several data hooks together, so — as with
|
|
13
|
+
* this package's other component-level rules (see `SelectionWizardHost.run-exit.test.ts`)
|
|
14
|
+
* — the wiring is read off the source. What it decides is proven where it can be
|
|
15
|
+
* run: `./organize-run-state.test.ts` for the ending the screen shows, and
|
|
16
|
+
* `../../hooks/useOrganizeJob.render.test.ts` for the poll it re-issues.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const source = readFileSync(resolve(here, "SelectionWizardHost.tsx"), "utf8");
|
|
21
|
+
|
|
22
|
+
const retryBody = (): string => {
|
|
23
|
+
const body = source.match(
|
|
24
|
+
/const retry = \(\): void => \{([\s\S]*?)\n\t\};/,
|
|
25
|
+
)?.[1];
|
|
26
|
+
assert.ok(body, "the run screen has no retry");
|
|
27
|
+
return body;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("retrying from an unknown status", () => {
|
|
31
|
+
it("looks at the job it already has", () => {
|
|
32
|
+
const body = retryBody();
|
|
33
|
+
const unknown = body.indexOf('run.state === "statusUnknown"');
|
|
34
|
+
assert.ok(unknown >= 0, "retry does not recognise an unknown status");
|
|
35
|
+
assert.match(body.slice(unknown), /organizeJob\.refreshStatus\(\)/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("decides that before it can reach anything that queues a second job", () => {
|
|
39
|
+
const body = retryBody();
|
|
40
|
+
assert.ok(
|
|
41
|
+
body.indexOf('run.state === "statusUnknown"') < body.indexOf("startJob("),
|
|
42
|
+
"a second back-apply can be queued over a job that is still running",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("reads what the job is doing rather than that something failed", () => {
|
|
47
|
+
assert.doesNotMatch(source, /organizeJob\.isError/);
|
|
48
|
+
assert.match(source, /organizeRunState\(\{/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
import { searchRuleAccountId } from "@/lib/organize/search-to-rule";
|
|
66
66
|
import type { OrganizeMatchPredicate } from "@/lib/organize/sender-fallback";
|
|
67
67
|
import { useWizardEntryValue, useWizardStep } from "@/lib/wizard-history";
|
|
68
|
+
import { organizeRunState } from "./organize-run-state";
|
|
68
69
|
|
|
69
70
|
const EMPTY_DRAFT: WizardDraft = { clauses: [], matchOperator: "any" };
|
|
70
71
|
|
|
@@ -696,29 +697,34 @@ function SelectionWizardSession({
|
|
|
696
697
|
sendCommit();
|
|
697
698
|
}, [blockedReason, current, goToStep, sendCommit]);
|
|
698
699
|
|
|
699
|
-
const jobSnapshot = useCallback(
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
700
|
+
const jobSnapshot = useCallback(
|
|
701
|
+
(ruleSaved: boolean): RunSnapshot => {
|
|
702
|
+
const progress = organizeJob.progress;
|
|
703
|
+
const state = organizeRunState({
|
|
704
|
+
failure: organizeJob.failure,
|
|
705
|
+
isStarting: organizeJob.isStarting,
|
|
706
|
+
isRunning: organizeJob.isRunning,
|
|
707
|
+
isDone: organizeJob.isDone,
|
|
708
|
+
failedCount: progress.failedCount,
|
|
709
|
+
ruleSaved,
|
|
710
|
+
});
|
|
711
|
+
if (
|
|
712
|
+
state === "saving" ||
|
|
713
|
+
state === "commitFailed" ||
|
|
714
|
+
state === "backApplyStartFailed"
|
|
715
|
+
) {
|
|
716
|
+
return { ...NOT_STARTED, state };
|
|
717
|
+
}
|
|
710
718
|
return {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
failed: progress.failedCount,
|
|
719
|
+
state,
|
|
720
|
+
matched: progress.matchedCount,
|
|
721
|
+
applied: progress.appliedCount,
|
|
722
|
+
failed: state === "backApplyFailed" ? progress.failedCount : 0,
|
|
723
|
+
failures: [],
|
|
715
724
|
};
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
return NOT_STARTED;
|
|
721
|
-
}, [organizeJob]);
|
|
725
|
+
},
|
|
726
|
+
[organizeJob],
|
|
727
|
+
);
|
|
722
728
|
|
|
723
729
|
// Every row the wizard has seen a description of, so a run that names what it
|
|
724
730
|
// did not reach can name it rather than listing an id.
|
|
@@ -794,21 +800,27 @@ function SelectionWizardSession({
|
|
|
794
800
|
return { ...NOT_STARTED, state: "commitFailed" };
|
|
795
801
|
if (!createFilter.isSuccess) return NOT_STARTED;
|
|
796
802
|
if (!backApplyDraft) return { ...NOT_STARTED, state: "filterSaved" };
|
|
797
|
-
|
|
798
|
-
return { ...NOT_STARTED, state: "backApplyStartFailed" };
|
|
799
|
-
}
|
|
800
|
-
return jobSnapshot();
|
|
803
|
+
return jobSnapshot(true);
|
|
801
804
|
}
|
|
802
805
|
if (committedScope === "all-like-these" && widenedRunsAsJob(verb)) {
|
|
803
|
-
return jobSnapshot();
|
|
806
|
+
return jobSnapshot(false);
|
|
804
807
|
}
|
|
805
808
|
return bulkSnapshot();
|
|
806
809
|
};
|
|
807
810
|
|
|
811
|
+
const run = runSnapshot();
|
|
812
|
+
|
|
808
813
|
// Retry stays on the run screen: it re-sends the same commit rather than
|
|
809
814
|
// walking back to Review, which would push an entry the wizard does not own
|
|
810
815
|
// and leave Cancel rewinding to a step instead of out.
|
|
811
816
|
const retry = (): void => {
|
|
817
|
+
// A status that could not be read is a job still running on the server, so
|
|
818
|
+
// the screen looks at that job again rather than queuing a second pass over
|
|
819
|
+
// the same mail (#526).
|
|
820
|
+
if (run.state === "statusUnknown") {
|
|
821
|
+
organizeJob.refreshStatus();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
812
824
|
// The predicate is re-resolved, not resumed: every verb it carries is
|
|
813
825
|
// idempotent, so the messages the first pass already reached are a no-op.
|
|
814
826
|
if (escalated) {
|
|
@@ -881,7 +893,6 @@ function SelectionWizardSession({
|
|
|
881
893
|
disabled: current !== "run",
|
|
882
894
|
});
|
|
883
895
|
|
|
884
|
-
const run = runSnapshot();
|
|
885
896
|
const sampleMessages = escalated
|
|
886
897
|
? escalatedSample.messages
|
|
887
898
|
: previewed
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A back-apply job reports two things that used to arrive as one error flag: it
|
|
3
|
+
* never started, or its status could not be read (#526). The run screen is only
|
|
4
|
+
* allowed to say nothing happened for the first.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import {
|
|
10
|
+
type OrganizeJobReading,
|
|
11
|
+
organizeRunState,
|
|
12
|
+
} from "./organize-run-state";
|
|
13
|
+
|
|
14
|
+
const reading = (
|
|
15
|
+
over: Partial<OrganizeJobReading> = {},
|
|
16
|
+
): OrganizeJobReading => ({
|
|
17
|
+
failure: undefined,
|
|
18
|
+
isStarting: false,
|
|
19
|
+
isRunning: false,
|
|
20
|
+
isDone: false,
|
|
21
|
+
failedCount: 0,
|
|
22
|
+
ruleSaved: false,
|
|
23
|
+
...over,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("organizeRunState", () => {
|
|
27
|
+
it("keeps a running job running when a status poll could not be read", () => {
|
|
28
|
+
const state = organizeRunState(
|
|
29
|
+
reading({
|
|
30
|
+
isRunning: true,
|
|
31
|
+
failure: { kind: "statusUnreadable", error: new Error("offline") },
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
assert.equal(state, "statusUnknown");
|
|
35
|
+
assert.notEqual(state, "commitFailed");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("keeps a finished job finished when a later poll fails", () => {
|
|
39
|
+
// Polling stops on a terminal state, so the failing read is a window-focus
|
|
40
|
+
// refetch over a job that already reported its counts.
|
|
41
|
+
assert.equal(
|
|
42
|
+
organizeRunState(
|
|
43
|
+
reading({
|
|
44
|
+
isDone: true,
|
|
45
|
+
failure: { kind: "statusUnreadable", error: new Error("offline") },
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
"backApplyComplete",
|
|
49
|
+
);
|
|
50
|
+
assert.equal(
|
|
51
|
+
organizeRunState(
|
|
52
|
+
reading({
|
|
53
|
+
isDone: true,
|
|
54
|
+
failedCount: 3,
|
|
55
|
+
failure: { kind: "statusUnreadable", error: new Error("offline") },
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
"backApplyFailed",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("says nothing happened only when the create itself failed", () => {
|
|
63
|
+
assert.equal(
|
|
64
|
+
organizeRunState(
|
|
65
|
+
reading({ failure: { kind: "startFailed", error: new Error("nope") } }),
|
|
66
|
+
),
|
|
67
|
+
"commitFailed",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("leaves a saved rule standing when its pass never started", () => {
|
|
72
|
+
assert.equal(
|
|
73
|
+
organizeRunState(
|
|
74
|
+
reading({
|
|
75
|
+
ruleSaved: true,
|
|
76
|
+
failure: { kind: "startFailed", error: new Error("nope") },
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
"backApplyStartFailed",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("reports a job that is starting or polling cleanly as running", () => {
|
|
84
|
+
assert.equal(
|
|
85
|
+
organizeRunState(reading({ isStarting: true })),
|
|
86
|
+
"backApplyRunning",
|
|
87
|
+
);
|
|
88
|
+
assert.equal(
|
|
89
|
+
organizeRunState(reading({ isRunning: true })),
|
|
90
|
+
"backApplyRunning",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("has nothing to report before a commit", () => {
|
|
95
|
+
assert.equal(organizeRunState(reading()), "saving");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RunState } from "@remit/ui";
|
|
2
|
+
import type { OrganizeJobFailure } from "@/hooks/useOrganizeJob";
|
|
3
|
+
|
|
4
|
+
/** What the run screen knows about a back-apply job it is reporting on. */
|
|
5
|
+
export interface OrganizeJobReading {
|
|
6
|
+
failure: OrganizeJobFailure | undefined;
|
|
7
|
+
isStarting: boolean;
|
|
8
|
+
isRunning: boolean;
|
|
9
|
+
isDone: boolean;
|
|
10
|
+
failedCount: number;
|
|
11
|
+
/**
|
|
12
|
+
* The pass belongs to a rule that already saved, so a pass that never started
|
|
13
|
+
* leaves the rule standing rather than leaving nothing behind.
|
|
14
|
+
*/
|
|
15
|
+
ruleSaved: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What the run screen says the job is doing. A status that could not be read is
|
|
20
|
+
* not a job that never started (#526), so what the job is doing is read before
|
|
21
|
+
* what failed: a dropped poll leaves a running pass running and a finished one
|
|
22
|
+
* finished, and only a create that never returned an id says nothing happened.
|
|
23
|
+
*/
|
|
24
|
+
export const organizeRunState = ({
|
|
25
|
+
failure,
|
|
26
|
+
isStarting,
|
|
27
|
+
isRunning,
|
|
28
|
+
isDone,
|
|
29
|
+
failedCount,
|
|
30
|
+
ruleSaved,
|
|
31
|
+
}: OrganizeJobReading): RunState => {
|
|
32
|
+
if (failure?.kind === "startFailed") {
|
|
33
|
+
return ruleSaved ? "backApplyStartFailed" : "commitFailed";
|
|
34
|
+
}
|
|
35
|
+
if (isDone) return failedCount > 0 ? "backApplyFailed" : "backApplyComplete";
|
|
36
|
+
if (failure?.kind === "statusUnreadable") return "statusUnknown";
|
|
37
|
+
if (isStarting || isRunning) return "backApplyRunning";
|
|
38
|
+
return "saving";
|
|
39
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOrganizeJob — the back-apply job seam. It reports two failures that are not
|
|
3
|
+
* the same fact (#526): a create that never returned a job id, and a status poll
|
|
4
|
+
* that could not be read over a job the server is already running. Looking at
|
|
5
|
+
* that job again is a separate move from starting one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { afterEach, describe, it } from "node:test";
|
|
10
|
+
import { act, createElement } from "react";
|
|
11
|
+
import type { OrganizeDraft } from "../lib/organize/organize-model";
|
|
12
|
+
import { createDomHarness, type DomHarness } from "../test-support/dom";
|
|
13
|
+
import { type HttpMock, mockFetch } from "../test-support/http";
|
|
14
|
+
import { useOrganizeJob } from "./useOrganizeJob";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A poll that could not be read reaches this screen only as a transport failure:
|
|
18
|
+
* `shouldEscalate` puts every answered non-2xx on the full-screen fatal page, so
|
|
19
|
+
* a status that is merely unreadable is a `fetch` that never landed.
|
|
20
|
+
*/
|
|
21
|
+
const dropped = (): never => {
|
|
22
|
+
throw new TypeError("fetch failed");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ACCOUNT = "acc-1";
|
|
26
|
+
const JOB = "job-1";
|
|
27
|
+
|
|
28
|
+
const DRAFT: OrganizeDraft = {
|
|
29
|
+
matchOperator: "Or",
|
|
30
|
+
literalClauses: [{ field: "From", value: "noreply@example.com" }],
|
|
31
|
+
moveMailboxId: "mbx-1",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let harness: DomHarness | undefined;
|
|
35
|
+
let http: HttpMock | undefined;
|
|
36
|
+
let job: ReturnType<typeof useOrganizeJob> | undefined;
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
harness?.close();
|
|
40
|
+
harness = undefined;
|
|
41
|
+
http?.restore();
|
|
42
|
+
http = undefined;
|
|
43
|
+
job = undefined;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function Probe() {
|
|
47
|
+
job = useOrganizeJob(ACCOUNT);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const current = (): ReturnType<typeof useOrganizeJob> => {
|
|
52
|
+
if (!job) throw new Error("useOrganizeJob is not mounted");
|
|
53
|
+
return job;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// A request round-trip needs a macrotask, not just a drained microtask queue.
|
|
57
|
+
const settle = async (): Promise<void> => {
|
|
58
|
+
await harness?.wait(1);
|
|
59
|
+
await harness?.flush();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Start a job the server accepts, then answer every status poll with `status`. */
|
|
63
|
+
const startJob = async (status: () => unknown): Promise<void> => {
|
|
64
|
+
http = mockFetch((call) => {
|
|
65
|
+
if (call.method === "POST") {
|
|
66
|
+
return { organizeJobId: JOB, state: "Pending" };
|
|
67
|
+
}
|
|
68
|
+
return status();
|
|
69
|
+
});
|
|
70
|
+
harness = createDomHarness();
|
|
71
|
+
harness.renderApp(createElement(Probe));
|
|
72
|
+
await act(async () => {
|
|
73
|
+
current().start(DRAFT);
|
|
74
|
+
});
|
|
75
|
+
await settle();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const posts = (): number =>
|
|
79
|
+
(http?.calls ?? []).filter((call) => call.method === "POST").length;
|
|
80
|
+
|
|
81
|
+
const statusPolls = (): number => (http?.to(`/organize/${JOB}`) ?? []).length;
|
|
82
|
+
|
|
83
|
+
describe("useOrganizeJob status reporting", () => {
|
|
84
|
+
it("reports a status poll it could not read as unreadable, over a job still running", async () => {
|
|
85
|
+
await startJob(dropped);
|
|
86
|
+
assert.equal(current().failure?.kind, "statusUnreadable");
|
|
87
|
+
assert.equal(current().isRunning, true);
|
|
88
|
+
assert.equal(current().isDone, false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("reports a create that never returned a job id as a start failure", async () => {
|
|
92
|
+
http = mockFetch(dropped);
|
|
93
|
+
harness = createDomHarness();
|
|
94
|
+
harness.renderApp(createElement(Probe));
|
|
95
|
+
await act(async () => {
|
|
96
|
+
current().start(DRAFT);
|
|
97
|
+
});
|
|
98
|
+
await settle();
|
|
99
|
+
assert.equal(current().failure?.kind, "startFailed");
|
|
100
|
+
assert.equal(current().isRunning, false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("re-polls the job it already has instead of starting a second one", async () => {
|
|
104
|
+
let readable = false;
|
|
105
|
+
await startJob(() =>
|
|
106
|
+
readable
|
|
107
|
+
? {
|
|
108
|
+
organizeJobId: JOB,
|
|
109
|
+
state: "Running",
|
|
110
|
+
matchedCount: 1284,
|
|
111
|
+
appliedCount: 40,
|
|
112
|
+
failedCount: 0,
|
|
113
|
+
}
|
|
114
|
+
: dropped(),
|
|
115
|
+
);
|
|
116
|
+
assert.equal(current().failure?.kind, "statusUnreadable");
|
|
117
|
+
const pollsBefore = statusPolls();
|
|
118
|
+
|
|
119
|
+
readable = true;
|
|
120
|
+
await act(async () => {
|
|
121
|
+
current().refreshStatus();
|
|
122
|
+
});
|
|
123
|
+
await settle();
|
|
124
|
+
|
|
125
|
+
assert.equal(posts(), 1);
|
|
126
|
+
assert.ok(statusPolls() > pollsBefore, "the existing job was polled again");
|
|
127
|
+
assert.equal(current().failure, undefined);
|
|
128
|
+
assert.equal(current().progress.matchedCount, 1284);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("stops reporting a job as running once it reaches a terminal state", async () => {
|
|
132
|
+
await startJob(() => ({
|
|
133
|
+
organizeJobId: JOB,
|
|
134
|
+
state: "Complete",
|
|
135
|
+
matchedCount: 1284,
|
|
136
|
+
appliedCount: 1284,
|
|
137
|
+
failedCount: 0,
|
|
138
|
+
}));
|
|
139
|
+
assert.equal(current().isDone, true);
|
|
140
|
+
assert.equal(current().failure, undefined);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -22,6 +22,25 @@ export interface OrganizeJobProgress {
|
|
|
22
22
|
errorMessage: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Why the job is not reporting, which is two separate facts (#526). A create
|
|
27
|
+
* that never returned an id means nothing was started; a status read that
|
|
28
|
+
* failed means a job is out there and this client cannot see how far it got.
|
|
29
|
+
*/
|
|
30
|
+
export interface OrganizeJobFailure {
|
|
31
|
+
kind: "startFailed" | "statusUnreadable";
|
|
32
|
+
error: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const organizeJobFailure = (
|
|
36
|
+
createError: unknown,
|
|
37
|
+
statusError: unknown,
|
|
38
|
+
): OrganizeJobFailure | undefined => {
|
|
39
|
+
if (createError) return { kind: "startFailed", error: createError };
|
|
40
|
+
if (statusError) return { kind: "statusUnreadable", error: statusError };
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
25
44
|
/**
|
|
26
45
|
* "All like these" — start a one-time retroactive back-apply (POST /organize)
|
|
27
46
|
* and poll its status to completion (GET /organize/{organizeJobId}). Polling
|
|
@@ -66,6 +85,13 @@ export const useOrganizeJob = (accountId: string | undefined) => {
|
|
|
66
85
|
[accountId, createJob],
|
|
67
86
|
);
|
|
68
87
|
|
|
88
|
+
const { refetch } = jobQuery;
|
|
89
|
+
// Looks at the job already in flight again. Distinct from `start`, which
|
|
90
|
+
// queues a second pass over the same mail.
|
|
91
|
+
const refreshStatus = useCallback(() => {
|
|
92
|
+
void refetch();
|
|
93
|
+
}, [refetch]);
|
|
94
|
+
|
|
69
95
|
const job = jobQuery.data;
|
|
70
96
|
const state = job?.state ?? createMutation.data?.state;
|
|
71
97
|
const isDone = isTerminalJobState(job?.state);
|
|
@@ -80,11 +106,11 @@ export const useOrganizeJob = (accountId: string | undefined) => {
|
|
|
80
106
|
|
|
81
107
|
return {
|
|
82
108
|
start,
|
|
109
|
+
refreshStatus,
|
|
83
110
|
progress,
|
|
84
111
|
isStarting: createMutation.isPending,
|
|
85
112
|
isRunning: !!organizeJobId && !isDone,
|
|
86
113
|
isDone,
|
|
87
|
-
|
|
88
|
-
error: createMutation.error ?? jobQuery.error,
|
|
114
|
+
failure: organizeJobFailure(createMutation.error, jobQuery.error),
|
|
89
115
|
};
|
|
90
116
|
};
|