@cratis/arc 21.4.0 → 21.6.0
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/dist/cjs/queries/QueryFor.js +50 -11
- package/dist/cjs/queries/QueryFor.js.map +1 -1
- package/dist/cjs/queries/QueryHttpRequest.js +0 -0
- package/dist/cjs/queries/QueryHttpRequest.js.map +1 -1
- package/dist/cjs/queries/isAbortError.js +18 -0
- package/dist/cjs/queries/isAbortError.js.map +1 -0
- package/dist/esm/queries/QueryFor.d.ts.map +1 -1
- package/dist/esm/queries/QueryFor.js +50 -11
- package/dist/esm/queries/QueryFor.js.map +1 -1
- package/dist/esm/queries/QueryHttpRequest.d.ts.map +1 -1
- package/dist/esm/queries/QueryHttpRequest.js +0 -0
- package/dist/esm/queries/QueryHttpRequest.js.map +1 -1
- package/dist/esm/queries/for_QueryFor/when_performing/with_a_nullish_transport_rejection.d.ts +2 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_a_nullish_transport_rejection.d.ts.map +1 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_a_nullish_transport_rejection.js +22 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_a_nullish_transport_rejection.js.map +1 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_an_aborted_request.d.ts +2 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_an_aborted_request.d.ts.map +1 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_an_aborted_request.js +21 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_an_aborted_request.js.map +1 -0
- package/dist/esm/queries/for_QueryFor/when_performing/with_fetch_error.js +7 -18
- package/dist/esm/queries/for_QueryFor/when_performing/with_fetch_error.js.map +1 -1
- package/dist/esm/queries/isAbortError.d.ts +2 -0
- package/dist/esm/queries/isAbortError.d.ts.map +1 -0
- package/dist/esm/queries/isAbortError.js +16 -0
- package/dist/esm/queries/isAbortError.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/queries/QueryFor.ts +51 -11
- package/queries/QueryHttpRequest.ts +0 -0
- package/queries/for_QueryFor/when_performing/with_a_nullish_transport_rejection.ts +40 -0
- package/queries/for_QueryFor/when_performing/with_an_aborted_request.ts +34 -0
- package/queries/for_QueryFor/when_performing/with_fetch_error.ts +13 -20
- package/queries/isAbortError.ts +15 -0
package/package.json
CHANGED
package/queries/QueryFor.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { ParameterDescriptor } from '../reflection/ParameterDescriptor';
|
|
|
14
14
|
import { ParametersHelper } from '../reflection/ParametersHelper';
|
|
15
15
|
import { QueryHttpMethod } from './QueryHttpMethod';
|
|
16
16
|
import { executeQueryHttpRequest } from './QueryHttpRequest';
|
|
17
|
+
import { isAbortError } from './isAbortError';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Represents an implementation of {@link IQueryFor}.
|
|
@@ -117,17 +118,56 @@ export abstract class QueryFor<TDataType, TParameters = object> implements IQuer
|
|
|
117
118
|
headers[Globals.microserviceHttpHeader] = this._microservice;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
let response: Response;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
response = await executeQueryHttpRequest(this._httpMethod, {
|
|
125
|
+
route: this.route,
|
|
126
|
+
apiBasePath: this._apiBasePath,
|
|
127
|
+
origin: this._origin,
|
|
128
|
+
args: (args as object) ?? {},
|
|
129
|
+
parameterValues,
|
|
130
|
+
paging: this.paging,
|
|
131
|
+
sorting: this.sorting,
|
|
132
|
+
headers,
|
|
133
|
+
signal: this.abortController.signal
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// An abort is not a failure - it is this query superseding its own in-flight request above.
|
|
137
|
+
// Rethrowing lets the caller discard the superseded request so it cannot settle over the
|
|
138
|
+
// newer one that now owns the result.
|
|
139
|
+
if (isAbortError(error)) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A dead network, a CORS rejection or a DNS failure never reaches the server, so it is
|
|
144
|
+
// neither an authorization nor a validation outcome - it is reported as an exception, with
|
|
145
|
+
// the default value as data, exactly as every other unsuccessful result from here does.
|
|
146
|
+
// Destructuring a nullish rejection value throws, which would make the `String(error)`
|
|
147
|
+
// fallback written for exactly that case unreachable - so it is coerced to an object first.
|
|
148
|
+
const { message } = (error ?? {}) as { message?: string };
|
|
149
|
+
const failure = {
|
|
150
|
+
...QueryResult.noSuccess,
|
|
151
|
+
data: this.defaultValue,
|
|
152
|
+
isSuccess: false,
|
|
153
|
+
isAuthorized: true,
|
|
154
|
+
isValid: true,
|
|
155
|
+
hasExceptions: true,
|
|
156
|
+
exceptionMessages: [message ?? String(error)],
|
|
157
|
+
// Left empty on purpose. Every result the server returns has its stack trace blanked
|
|
158
|
+
// unless exception detail is explicitly exposed, and consumers are told to forward
|
|
159
|
+
// this field to a logger - so filling it in here would make a failure that never
|
|
160
|
+
// reached the server the one case that escapes that policy. The message is what
|
|
161
|
+
// diagnoses a transport failure; the browser's own frames add nothing.
|
|
162
|
+
exceptionStackTrace: ''
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// `hasData` is a prototype getter, and a spread copies own properties only - so the literal
|
|
166
|
+
// above would hand every consumer of this public `IQueryResult` member `undefined` instead
|
|
167
|
+
// of a boolean. Restoring the prototype makes it a real {@link QueryResult} again while
|
|
168
|
+
// leaving every field value exactly as it is.
|
|
169
|
+
return Object.setPrototypeOf(failure, QueryResult.prototype) as QueryResult<TDataType>;
|
|
170
|
+
}
|
|
131
171
|
|
|
132
172
|
try {
|
|
133
173
|
const result = await response.json();
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { a_query_for } from '../given/a_query_for';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
|
|
7
|
+
import * as sinon from 'sinon';
|
|
8
|
+
import { createFetchHelper } from '../../../helpers/fetchHelper';
|
|
9
|
+
import { QueryResult } from '../../QueryResult';
|
|
10
|
+
|
|
11
|
+
describe('with a nullish transport rejection', given(a_query_for, context => {
|
|
12
|
+
let outcome: { resolved: boolean; result?: QueryResult<string> };
|
|
13
|
+
let fetchStub: sinon.SinonStub;
|
|
14
|
+
let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
fetchHelper = createFetchHelper();
|
|
18
|
+
fetchStub = fetchHelper.stubFetch();
|
|
19
|
+
|
|
20
|
+
// Nothing guarantees a rejection value is an object - `Promise.reject()` and a thrown `undefined`
|
|
21
|
+
// both reach here, and an interceptor or a polyfilled fetch can produce either.
|
|
22
|
+
fetchStub.callsFake(() => Promise.reject(undefined));
|
|
23
|
+
|
|
24
|
+
context.query.setOrigin('https://api.example.com');
|
|
25
|
+
|
|
26
|
+
outcome = await context.query.perform({ id: 'test-id' }).then(
|
|
27
|
+
result => ({ resolved: true, result }),
|
|
28
|
+
() => ({ resolved: false }));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
fetchHelper.restore();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should resolve rather than reject', () => outcome.resolved.should.be.true);
|
|
36
|
+
|
|
37
|
+
it('should report that it has exceptions', () => outcome.result!.hasExceptions.should.be.true);
|
|
38
|
+
|
|
39
|
+
it('should describe the rejection it could not read a message from', () => outcome.result!.exceptionMessages.should.deep.equal(['undefined']));
|
|
40
|
+
}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { a_query_for } from '../given/a_query_for';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
|
|
7
|
+
import * as sinon from 'sinon';
|
|
8
|
+
import { createFetchHelper } from '../../../helpers/fetchHelper';
|
|
9
|
+
|
|
10
|
+
describe('with an aborted request', given(a_query_for, context => {
|
|
11
|
+
let rejection: unknown;
|
|
12
|
+
let fetchStub: sinon.SinonStub;
|
|
13
|
+
let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
fetchHelper = createFetchHelper();
|
|
17
|
+
fetchStub = fetchHelper.stubFetch();
|
|
18
|
+
fetchStub.rejects(Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }));
|
|
19
|
+
|
|
20
|
+
context.query.setOrigin('https://api.example.com');
|
|
21
|
+
|
|
22
|
+
// An abort only ever happens because a newer request superseded this one, so the outcome is
|
|
23
|
+
// recorded rather than swallowed - the assertions below are on what actually came back.
|
|
24
|
+
rejection = await context.query.perform({ id: 'test-id' }).then(() => undefined, error => error);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fetchHelper.restore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should reject so the superseded request cannot settle a result', () => (rejection !== undefined).should.be.true);
|
|
32
|
+
|
|
33
|
+
it('should reject with the abort error', () => (rejection as Error).name.should.equal('AbortError'));
|
|
34
|
+
}));
|
|
@@ -14,37 +14,30 @@ describe('with fetch error', given(a_query_for, context => {
|
|
|
14
14
|
let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
|
|
15
15
|
|
|
16
16
|
beforeEach(async () => {
|
|
17
|
-
// Setup fetch mock to reject
|
|
18
17
|
fetchHelper = createFetchHelper();
|
|
19
18
|
fetchStub = fetchHelper.stubFetch();
|
|
20
19
|
fetchStub.rejects(new Error('Network error'));
|
|
21
20
|
|
|
22
21
|
context.query.setOrigin('https://api.example.com');
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
// Call perform with valid arguments - should handle the error and return no success
|
|
26
|
-
result = await context.query.perform({ id: 'test-id' });
|
|
27
|
-
} catch {
|
|
28
|
-
// If QueryFor doesn't handle the error, we'll create a default response
|
|
29
|
-
const noSuccess = { ...QueryResult.noSuccess, data: context.query.defaultValue } as QueryResult<string>;
|
|
30
|
-
result = noSuccess;
|
|
31
|
-
}
|
|
23
|
+
result = await context.query.perform({ id: 'test-id' });
|
|
32
24
|
});
|
|
33
25
|
|
|
34
26
|
afterEach(() => {
|
|
35
27
|
fetchHelper.restore();
|
|
36
28
|
});
|
|
37
29
|
|
|
38
|
-
it('should
|
|
39
|
-
result.isSuccess.should.be.false;
|
|
40
|
-
});
|
|
30
|
+
it('should resolve rather than reject', () => (result !== undefined).should.be.true);
|
|
41
31
|
|
|
42
|
-
it('should return
|
|
43
|
-
result.data.should.equal('');
|
|
44
|
-
});
|
|
32
|
+
it('should return an unsuccessful result', () => result.isSuccess.should.be.false);
|
|
45
33
|
|
|
46
|
-
it('should
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
it('should report that it has exceptions', () => result.hasExceptions.should.be.true);
|
|
35
|
+
|
|
36
|
+
it('should carry the transport error message', () => result.exceptionMessages.should.deep.equal(['Network error']));
|
|
37
|
+
|
|
38
|
+
it('should return the default value as data', () => result.data.should.equal(''));
|
|
39
|
+
|
|
40
|
+
// `hasData` is part of the public `IQueryResult` contract, and a consumer awaiting `perform()`
|
|
41
|
+
// directly reads it off this object rather than off the `QueryResultWithState` the hooks build.
|
|
42
|
+
it('should expose hasData as a boolean', () => (typeof result.hasData).should.equal('boolean'));
|
|
43
|
+
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Determines whether an error is the rejection produced by aborting a request through an
|
|
6
|
+
* {@link AbortSignal} - as opposed to a genuine transport failure.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately kept out of the `queries` barrel: it is an internal detail shared by the query
|
|
9
|
+
* transport and {@link QueryFor}, not public surface for consumers to depend on.
|
|
10
|
+
* @param error The error to inspect.
|
|
11
|
+
* @returns True if the error represents an aborted request, false otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export function isAbortError(error: unknown): boolean {
|
|
14
|
+
return (error as { name?: string })?.name === 'AbortError';
|
|
15
|
+
}
|