Skip to content

Commit de4da09

Browse files
committed
tweak reason, add test
1 parent 31a5287 commit de4da09

2 files changed

Lines changed: 65 additions & 35 deletions

File tree

src/link/http/BaseHttpLink.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,19 @@ export class BaseHttpLink extends ApolloLink {
9595
controller = undefined;
9696
};
9797
if (options.signal) {
98-
const externalSignal = options.signal;
98+
const externalSignal: AbortSignal = options.signal;
9999
// in an ideal world we could use `AbortSignal.any` here, but
100100
// React Native uses https://github.com/mysticatea/abort-controller as
101101
// a polyfill for `AbortController`, and it does not support `AbortSignal.any`.
102-
const abort = controller.abort.bind(controller);
103-
externalSignal.addEventListener("abort", abort, { once: true });
102+
103+
const listener = () => {
104+
controller?.abort(externalSignal.reason);
105+
};
106+
externalSignal.addEventListener("abort", listener, { once: true });
104107
cleanupController = () => {
105108
controller = undefined;
106109
// on cleanup, we need to stop listening to `options.signal` to avoid memory leaks
107-
externalSignal.removeEventListener("abort", abort);
110+
externalSignal.removeEventListener("abort", listener);
108111
cleanupController = noop;
109112
};
110113
// react native also does not support the addEventListener `signal` option

src/link/http/__tests__/HttpLink.ts

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,52 +1159,79 @@ describe("HttpLink", () => {
11591159
expect(abortControllers[0].signal.aborted).toBe(true);
11601160
});
11611161

1162-
it("a passed-in signal will be forwarded to the `fetch` call and not be overwritten by an internally-created one", () => {
1163-
const fetch = jest.fn(async (_uri, _options) =>
1164-
Response.json({ data: { stub: { id: "foo" } } }, { status: 200 })
1165-
);
1166-
const externalAbortController = new AbortController();
1162+
it("a passed-in signal that is cancelled will fail the observable with an `AbortError`", async () => {
1163+
try {
1164+
fetchMock.restore();
1165+
fetchMock.postOnce(
1166+
"data",
1167+
async () => '{ "data": { "stub": { "id": "foo" } } }',
1168+
{ delay: 100 }
1169+
);
11671170

1168-
const link = createHttpLink({
1169-
uri: "data",
1170-
fetch,
1171-
fetchOptions: { signal: externalAbortController.signal },
1172-
});
1171+
const externalAbortController = new AbortController();
1172+
const abortControllers = trackGlobalAbortControllers();
11731173

1174-
const sub = execute(link, { query: sampleQuery }).subscribe(
1175-
failingObserver
1176-
);
1177-
sub.unsubscribe();
1174+
const link = createHttpLink({
1175+
uri: "/data",
1176+
});
11781177

1179-
expect(fetch.mock.calls.length).toBe(1);
1180-
expect(fetch.mock.calls[0][1]).toEqual(
1181-
expect.objectContaining({ signal: externalAbortController.signal })
1182-
);
1178+
const observable = execute(link, {
1179+
query: sampleQuery,
1180+
context: {
1181+
fetchOptions: { signal: externalAbortController.signal },
1182+
},
1183+
});
1184+
1185+
const internalAbortController = abortControllers[0];
1186+
1187+
const stream = new ObservableStream(observable);
1188+
const externalReason = new Error("External abort reason");
1189+
1190+
externalAbortController.abort(externalReason);
1191+
1192+
await expect(stream).toEmitError(
1193+
// this not being `externalReason` is a quirk of `fetch-mock`:
1194+
// https://github.com/wheresrhys/fetch-mock/blob/605ec0afa6a5ff35066b9e01a9bcd688f3c25ce0/packages/fetch-mock/src/Router.ts#L164-L167
1195+
new DOMException("The operation was aborted.", "AbortError")
1196+
);
1197+
1198+
expect(externalAbortController).not.toBe(internalAbortController);
1199+
expect(externalAbortController.signal.aborted).toBe(true);
1200+
expect(externalAbortController.signal.reason).toBe(externalReason);
1201+
expect(internalAbortController.signal.aborted).toBe(true);
1202+
expect(internalAbortController.signal.reason).toBe(externalReason);
1203+
} finally {
1204+
fetchMock.restore();
1205+
}
11831206
});
11841207

1185-
it("a passed-in signal that is cancelled will fail the observable with an `AbortError`", async () => {
1208+
it("a passed-in signal will not fully overwrite the internally created one", () => {
11861209
try {
1210+
const externalAbortController = new AbortController();
1211+
const abortControllers = trackGlobalAbortControllers();
1212+
11871213
fetchMock.restore();
11881214
fetchMock.postOnce(
11891215
"data",
11901216
async () => '{ "data": { "stub": { "id": "foo" } } }'
11911217
);
11921218

1193-
const externalAbortController = new AbortController();
1194-
1195-
const link = createHttpLink({
1219+
const link = new HttpLink({
11961220
uri: "/data",
1197-
fetchOptions: { signal: externalAbortController.signal },
11981221
});
11991222

1200-
const error = await new Promise<Error>((resolve) => {
1201-
execute(link, { query: sampleQuery }).subscribe({
1202-
...failingObserver,
1203-
error: resolve,
1204-
});
1205-
externalAbortController.abort();
1206-
});
1207-
expect(error.name).toBe("AbortError");
1223+
const sub = execute(link, {
1224+
query: sampleQuery,
1225+
context: {
1226+
fetchOptions: { signal: externalAbortController.signal },
1227+
},
1228+
}).subscribe(failingObserver);
1229+
const internalAbortController = abortControllers[0];
1230+
1231+
sub.unsubscribe();
1232+
1233+
expect(externalAbortController.signal.aborted).toBe(false);
1234+
expect(internalAbortController.signal.aborted).toBe(true);
12081235
} finally {
12091236
fetchMock.restore();
12101237
}

0 commit comments

Comments
 (0)