@remit/web-client 0.0.264 → 0.0.265

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.264",
3
+ "version": "0.0.265",
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": {
@@ -41,17 +41,23 @@ const ComposeContext = createContext<ComposeContextValue | undefined>(
41
41
  const POLL_INTERVAL_MS = 2000;
42
42
  const MAX_POLL_DURATION_MS = 60_000;
43
43
 
44
+ /**
45
+ * `sent` is not one of these. It is the window between SMTP handing the message
46
+ * over and the APPEND either filing it — row deleted, 404 — or settling it
47
+ * `unfiled`, and the outbox list renders no `sent` row (`isOutboxListRow`). A
48
+ * watch that stopped there invalidated the list over a row nothing displays and
49
+ * never asked again, so the `unfiled` a beat later reached no screen: #824's
50
+ * message the sender can reach in no view, arrived at from the client's side.
51
+ * A row stranded at `sent` by a delete that failed polls to the cap below,
52
+ * which is what the cap is for.
53
+ */
44
54
  const isSettledStatus = (status: string | undefined): boolean =>
45
- status === "sent" ||
46
- status === "unfiled" ||
47
- status === "failed" ||
48
- status === "blocked";
55
+ status === "unfiled" || status === "failed" || status === "blocked";
49
56
 
50
57
  /**
51
58
  * A send that lands is filed and then forgotten: the worker APPENDs the message
52
59
  * to Sent and drops the outbox row, so the row's absence is the settled state
53
- * and the 404 is the confirmation. `sent` lives for under a second and this poll
54
- * runs every two, so the 404 is the outcome it normally reads.
60
+ * and the 404 is the confirmation.
55
61
  *
56
62
  * Only the 404 is this call site's to own. A 401 or a 403 here is a session that
57
63
  * lapsed under a send, which the app owes the user an answer about.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Issue #1040: the send watch counted `sent` as its answer and came down on it.
3
+ *
4
+ * `sent` is the window between SMTP handing the message over and the APPEND
5
+ * either filing it — row deleted, 404 — or settling it `unfiled`. It is a few
6
+ * tens of milliseconds wide and the watch's first read lands inside it perhaps
7
+ * one send in twenty, which is where the e2e flake came from. What the user got
8
+ * when it did land there is worse than a flake: the watch stopped, invalidated
9
+ * the outbox list over a row the list does not render, and never asked again —
10
+ * so a message that went out unfiled was on no screen at all, which is the very
11
+ * state #824 exists to prevent.
12
+ */
13
+
14
+ import assert from "node:assert/strict";
15
+ import { afterEach, describe, it } from "node:test";
16
+ import { outboxOperationsListOutboxMessagesQueryKey } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
17
+ import { createElement, useEffect } from "react";
18
+ import { createDomHarness, type DomHarness } from "../../test-support/dom";
19
+ import { type HttpMock, mockFetch } from "../../test-support/http";
20
+ import { ComposeProvider, useCompose } from "./ComposeProvider";
21
+
22
+ const OUTBOX_MESSAGE_ID = "ob-1040";
23
+ const POLL_INTERVAL_MS = 2000;
24
+
25
+ const UNFILED_REASON =
26
+ "Sent, but not filed: this account has no folder appointed to the Sent role.";
27
+
28
+ let harness: DomHarness | undefined;
29
+ let http: HttpMock | undefined;
30
+ let status = "sent";
31
+
32
+ afterEach(() => {
33
+ harness?.close();
34
+ harness = undefined;
35
+ http?.restore();
36
+ http = undefined;
37
+ status = "sent";
38
+ });
39
+
40
+ const polls = (): number => (http?.to(OUTBOX_MESSAGE_ID) ?? []).length;
41
+
42
+ const outboxListIsStale = (): boolean =>
43
+ harness?.queryClient
44
+ .getQueryCache()
45
+ .find({ queryKey: outboxOperationsListOutboxMessagesQueryKey() })?.state
46
+ .isInvalidated === true;
47
+
48
+ const Sender = () => {
49
+ const { startSendPolling } = useCompose();
50
+
51
+ useEffect(() => {
52
+ startSendPolling(OUTBOX_MESSAGE_ID);
53
+ }, [startSendPolling]);
54
+
55
+ return null;
56
+ };
57
+
58
+ const mount = async (): Promise<void> => {
59
+ http = mockFetch((call) => {
60
+ if (call.path.endsWith(OUTBOX_MESSAGE_ID)) {
61
+ return {
62
+ outboxMessageId: OUTBOX_MESSAGE_ID,
63
+ status,
64
+ lastError: status === "unfiled" ? UNFILED_REASON : undefined,
65
+ };
66
+ }
67
+ return { messages: [] };
68
+ });
69
+
70
+ harness = createDomHarness();
71
+ harness.queryClient.setQueryData(
72
+ outboxOperationsListOutboxMessagesQueryKey(),
73
+ { messages: [] },
74
+ );
75
+ harness.renderApp(
76
+ createElement(ComposeProvider, null, createElement(Sender)),
77
+ );
78
+ await harness.flush();
79
+ };
80
+
81
+ const reachedPolls = async (count: number): Promise<void> => {
82
+ await harness?.waitFor(
83
+ () => polls() >= count,
84
+ `the watch to have read the row ${count} times`,
85
+ POLL_INTERVAL_MS * count + POLL_INTERVAL_MS,
86
+ );
87
+ };
88
+
89
+ describe("a send whose filing is still owed (#1040)", () => {
90
+ it("keeps asking while the row reads sent", async () => {
91
+ await mount();
92
+ await reachedPolls(1);
93
+
94
+ assert.ok(
95
+ !outboxListIsStale(),
96
+ "nothing was settled, so nothing refetched",
97
+ );
98
+
99
+ await reachedPolls(3);
100
+ assert.ok(
101
+ !outboxListIsStale(),
102
+ "`sent` is not an answer the watch stops on",
103
+ );
104
+ });
105
+
106
+ it("stops on the unfiled row, and only then refetches the list", async () => {
107
+ await mount();
108
+ await reachedPolls(2);
109
+
110
+ status = "unfiled";
111
+ await harness?.waitFor(
112
+ () => outboxListIsStale(),
113
+ "the watch to settle on the unfiled row and invalidate the list",
114
+ POLL_INTERVAL_MS * 3,
115
+ );
116
+
117
+ const settled = polls();
118
+ await harness?.wait(POLL_INTERVAL_MS * 2);
119
+ assert.equal(polls(), settled, "the settled row ended the watch");
120
+ });
121
+ });