@remit/web-client 0.0.241 → 0.0.243
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.243",
|
|
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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coming back to a tab always leaves the poll loop with a next tick (#1031).
|
|
3
|
+
*
|
|
4
|
+
* A tick that fires while the tab is hidden deliberately does not reschedule —
|
|
5
|
+
* `visibilitychange` is the only thing that resumes the loop. While that resume
|
|
6
|
+
* was itself conditional on a poll being due, the resume could be skipped for
|
|
7
|
+
* good: press refresh (which opens the hot 30s window), hide the tab shortly
|
|
8
|
+
* after a hot tick, let the window lapse while away, and the return is measured
|
|
9
|
+
* against the ambient interval the tab never slept through. Nothing re-armed,
|
|
10
|
+
* and the tab stopped polling until the account list changed or the layout
|
|
11
|
+
* remounted.
|
|
12
|
+
*
|
|
13
|
+
* At the default the ambient and hot intervals are equal and the loop is safe
|
|
14
|
+
* either way, so these drive a deployment that lengthened
|
|
15
|
+
* `mailboxPollIntervalSeconds` — the only shape where the bug is reachable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import { afterEach, beforeEach, describe, mock, test } from "node:test";
|
|
20
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
21
|
+
import { act, createElement } from "react";
|
|
22
|
+
import {
|
|
23
|
+
__resetHotSyncWindow,
|
|
24
|
+
HOT_POLL_INTERVAL_MS,
|
|
25
|
+
HOT_WINDOW_MS,
|
|
26
|
+
startHotSyncWindow,
|
|
27
|
+
} from "@/lib/hot-sync-window";
|
|
28
|
+
import { createDomHarness, type DomHarness } from "@/test-support/dom";
|
|
29
|
+
import { makeAccount } from "@/test-support/fixtures";
|
|
30
|
+
import { type HttpMock, mockFetch } from "@/test-support/http";
|
|
31
|
+
|
|
32
|
+
const ACCOUNT_ID = "acc-1";
|
|
33
|
+
const AMBIENT_SECONDS = 600;
|
|
34
|
+
const AMBIENT_MS = AMBIENT_SECONDS * 1000;
|
|
35
|
+
const T0 = 1_700_000_000_000;
|
|
36
|
+
|
|
37
|
+
// The hook resolves the ambient interval once, at module load, so the config has
|
|
38
|
+
// to be in place before it is imported.
|
|
39
|
+
globalThis.__REMIT_CONFIG__ = {
|
|
40
|
+
mailboxPollIntervalSeconds: String(AMBIENT_SECONDS),
|
|
41
|
+
};
|
|
42
|
+
const { __resetStaleAccountSyncGuard, POLL_INTERVAL_MS, useStaleAccountSync } =
|
|
43
|
+
await import("./useStaleAccountSync.js");
|
|
44
|
+
|
|
45
|
+
let harness: DomHarness | undefined;
|
|
46
|
+
let http: HttpMock | undefined;
|
|
47
|
+
|
|
48
|
+
function PollLoop() {
|
|
49
|
+
useStaleAccountSync([makeAccount({ accountId: ACCOUNT_ID, lastSyncAt: T0 })]);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* jsdom reports "prerender" unless it is pretending to be visual, and the
|
|
55
|
+
* shared environment deliberately does not — so a spec about a tab being looked
|
|
56
|
+
* at has to say so.
|
|
57
|
+
*/
|
|
58
|
+
const setVisibility = (state: "hidden" | "visible"): void => {
|
|
59
|
+
Object.defineProperty(document, "visibilityState", {
|
|
60
|
+
configurable: true,
|
|
61
|
+
get: () => state,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Enough microtask turns for a dispatched poll to reach the fetch seam. */
|
|
66
|
+
const settle = async (dom: DomHarness): Promise<void> => {
|
|
67
|
+
for (let round = 0; round < 3; round += 1) await dom.flush();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mount = async (): Promise<DomHarness> => {
|
|
71
|
+
http = mockFetch(() => ({ status: "queued" }));
|
|
72
|
+
const dom = createDomHarness();
|
|
73
|
+
harness = dom;
|
|
74
|
+
dom.render(
|
|
75
|
+
createElement(
|
|
76
|
+
QueryClientProvider,
|
|
77
|
+
{ client: dom.queryClient },
|
|
78
|
+
createElement(PollLoop),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
await settle(dom);
|
|
82
|
+
return dom;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const advance = async (dom: DomHarness, ms: number): Promise<void> => {
|
|
86
|
+
await act(async () => {
|
|
87
|
+
mock.timers.tick(ms);
|
|
88
|
+
});
|
|
89
|
+
await settle(dom);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const goVisible = async (dom: DomHarness): Promise<void> => {
|
|
93
|
+
setVisibility("visible");
|
|
94
|
+
dom.dispatch(dom.document, new Event("visibilitychange"));
|
|
95
|
+
await settle(dom);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const goHidden = async (dom: DomHarness): Promise<void> => {
|
|
99
|
+
setVisibility("hidden");
|
|
100
|
+
dom.dispatch(dom.document, new Event("visibilitychange"));
|
|
101
|
+
await settle(dom);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Every background sync this tab has fired since it mounted. */
|
|
105
|
+
const polls = (): number => http?.to(`/${ACCOUNT_ID}/sync`).length ?? 0;
|
|
106
|
+
|
|
107
|
+
describe("the poll loop across hide and show, on a long ambient interval", () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
mock.timers.enable({ apis: ["setTimeout", "Date"], now: T0 });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
mock.timers.reset();
|
|
114
|
+
harness?.close();
|
|
115
|
+
harness = undefined;
|
|
116
|
+
http?.restore();
|
|
117
|
+
http = undefined;
|
|
118
|
+
__resetHotSyncWindow();
|
|
119
|
+
__resetStaleAccountSyncGuard();
|
|
120
|
+
Reflect.deleteProperty(document, "visibilityState");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("the deployment under test really is on a lengthened interval", () => {
|
|
124
|
+
assert.equal(POLL_INTERVAL_MS, AMBIENT_MS);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("keeps polling after a refresh press, a hidden hot window and a return", async () => {
|
|
128
|
+
setVisibility("visible");
|
|
129
|
+
const dom = await mount();
|
|
130
|
+
|
|
131
|
+
// The press: the hot window opens and the loop drops onto its cadence.
|
|
132
|
+
startHotSyncWindow();
|
|
133
|
+
await advance(dom, HOT_POLL_INTERVAL_MS);
|
|
134
|
+
assert.equal(polls(), 1, "the hot cadence polled once");
|
|
135
|
+
|
|
136
|
+
// Away before the next hot tick, and back only after the window lapsed.
|
|
137
|
+
await goHidden(dom);
|
|
138
|
+
await advance(dom, HOT_WINDOW_MS);
|
|
139
|
+
assert.equal(polls(), 1, "a hidden tab polled");
|
|
140
|
+
|
|
141
|
+
await goVisible(dom);
|
|
142
|
+
assert.equal(
|
|
143
|
+
polls(),
|
|
144
|
+
1,
|
|
145
|
+
"the return polled a tab that is not due on the ambient interval",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await advance(dom, AMBIENT_MS);
|
|
149
|
+
assert.equal(polls(), 2, "the tab came back with no tick scheduled");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("polls on return once the ambient interval has passed", async () => {
|
|
153
|
+
setVisibility("visible");
|
|
154
|
+
const dom = await mount();
|
|
155
|
+
|
|
156
|
+
await goHidden(dom);
|
|
157
|
+
await advance(dom, AMBIENT_MS);
|
|
158
|
+
assert.equal(polls(), 0, "a hidden tab polled");
|
|
159
|
+
|
|
160
|
+
await goVisible(dom);
|
|
161
|
+
assert.equal(polls(), 1, "a tab due a poll did not catch up on return");
|
|
162
|
+
|
|
163
|
+
await advance(dom, AMBIENT_MS);
|
|
164
|
+
assert.equal(polls(), 2, "the catch-up left no next tick behind it");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -275,21 +275,28 @@ export const useStaleAccountSync = (
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
// Hidden: don't reschedule. `handleVisibilityChange` resumes the loop
|
|
278
|
-
// (with an immediate catch-up poll) once the tab is
|
|
279
|
-
// A hot window running out while the tab is away costs
|
|
280
|
-
// there is nobody watching a list to keep current.
|
|
278
|
+
// (with an immediate catch-up poll if one is due) once the tab is
|
|
279
|
+
// visible again. A hot window running out while the tab is away costs
|
|
280
|
+
// nothing — there is nobody watching a list to keep current.
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
// A tick that fired while the tab was hidden left no timer behind, so
|
|
284
|
+
// coming back is the only thing that can re-arm the loop — it does so
|
|
285
|
+
// unconditionally. Due-ness decides whether this tab polls *now*, never
|
|
286
|
+
// whether a next tick exists: a tab that went to sleep on the hot cadence
|
|
287
|
+
// and came back after the window lapsed is not due against a longer
|
|
288
|
+
// ambient interval, and used to stop polling for good (#1031).
|
|
283
289
|
const handleVisibilityChange = () => {
|
|
284
290
|
if (document.visibilityState !== "visible") return;
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
)
|
|
291
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
292
|
+
const now = Date.now();
|
|
293
|
+
const intervalMs = currentPollIntervalMs(now);
|
|
294
|
+
if (hasPollIntervalElapsed(now, lastPollAt, intervalMs)) {
|
|
295
|
+
poll();
|
|
296
|
+
timeoutId = setTimeout(tick, intervalMs);
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
290
|
-
|
|
291
|
-
poll();
|
|
292
|
-
timeoutId = setTimeout(tick, currentPollIntervalMs());
|
|
299
|
+
timeoutId = setTimeout(tick, lastPollAt + intervalMs - now);
|
|
293
300
|
};
|
|
294
301
|
|
|
295
302
|
// A press opens the hot window, but this loop may already be asleep on a
|
|
@@ -108,7 +108,7 @@ function AccountFolders({ account }: { account: RemitImapAccountResponse }) {
|
|
|
108
108
|
[mailboxes, roleMap, translator, account.folderAppointments],
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
-
const handleAppoint = (role: FolderRole, mailboxId: string
|
|
111
|
+
const handleAppoint = (role: FolderRole, mailboxId: string) => {
|
|
112
112
|
appointMutation.mutate({
|
|
113
113
|
path: { accountId, role: NAV_ROLE_TO_CANONICAL[role] },
|
|
114
114
|
body: { mailboxId },
|