@tstdl/base 0.93.126 → 0.93.128
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/api/client/client.js +45 -9
- package/api/client/tests/api-client.test.d.ts +1 -0
- package/api/client/tests/api-client.test.js +194 -0
- package/api/types.d.ts +34 -2
- package/api/types.js +9 -2
- package/authentication/client/authentication.service.js +30 -11
- package/authentication/client/http-client.middleware.js +10 -3
- package/authentication/server/authentication.service.d.ts +12 -0
- package/authentication/server/authentication.service.js +14 -2
- package/authentication/tests/authentication.client-error-handling.test.js +23 -66
- package/authentication/tests/authentication.client-service-refresh.test.js +14 -14
- package/cancellation/token.d.ts +6 -0
- package/cancellation/token.js +8 -0
- package/http/client/adapters/undici.adapter.js +0 -2
- package/http/client/http-client-request.d.ts +2 -0
- package/http/client/http-client-request.js +4 -0
- package/http/client/http-client-response.d.ts +1 -1
- package/http/client/http-client-response.js +3 -2
- package/http/utils.d.ts +6 -0
- package/http/utils.js +71 -0
- package/injector/graph.js +27 -6
- package/injector/injector.js +2 -0
- package/mail/drizzle/0000_numerous_the_watchers.sql +8 -0
- package/mail/drizzle/meta/0000_snapshot.json +1 -32
- package/mail/drizzle/meta/_journal.json +2 -9
- package/object-storage/s3/tests/s3.object-storage.integration.test.js +22 -53
- package/orm/tests/repository-expiration.test.js +3 -3
- package/package.json +1 -1
- package/rate-limit/tests/postgres-rate-limiter.test.js +9 -7
- package/task-queue/tests/complex.test.js +22 -22
- package/task-queue/tests/dependencies.test.js +15 -13
- package/task-queue/tests/queue.test.js +13 -13
- package/task-queue/tests/worker.test.js +12 -12
- package/testing/integration-setup.d.ts +2 -0
- package/testing/integration-setup.js +13 -7
- package/utils/backoff.d.ts +27 -3
- package/utils/backoff.js +31 -9
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/retry-with-backoff.d.ts +22 -0
- package/utils/retry-with-backoff.js +64 -0
- package/utils/tests/backoff.test.d.ts +1 -0
- package/utils/tests/backoff.test.js +41 -0
- package/utils/tests/retry-with-backoff.test.d.ts +1 -0
- package/utils/tests/retry-with-backoff.test.js +49 -0
- package/mail/drizzle/0000_previous_malcolm_colcord.sql +0 -13
- package/mail/drizzle/0001_flimsy_bloodscream.sql +0 -5
- package/mail/drizzle/meta/0001_snapshot.json +0 -69
|
@@ -70,6 +70,12 @@ describe('AuthenticationClientService Error Handling & Stuck States', () => {
|
|
|
70
70
|
afterEach(async () => {
|
|
71
71
|
await service.dispose();
|
|
72
72
|
});
|
|
73
|
+
function setupServiceWithToken(iatOffset = -10, expOffset = 5) {
|
|
74
|
+
const now = Math.floor(Date.now() / 1000);
|
|
75
|
+
const initialToken = { iat: now + iatOffset, exp: now + expOffset, jti: 'initial' };
|
|
76
|
+
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
77
|
+
service = injector.resolve(AuthenticationClientService);
|
|
78
|
+
}
|
|
73
79
|
test('Corrupt Storage: should handle invalid JSON in storage gracefully', async () => {
|
|
74
80
|
// initialize with corrupt token
|
|
75
81
|
globalThis.localStorage.setItem('AuthenticationService:token', '{ invalid-json');
|
|
@@ -77,84 +83,35 @@ describe('AuthenticationClientService Error Handling & Stuck States', () => {
|
|
|
77
83
|
expect(service.token()).toBeUndefined();
|
|
78
84
|
expect(mockLogger.warn).toHaveBeenCalled(); // Should warn about parse error
|
|
79
85
|
});
|
|
80
|
-
test(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
test.each([
|
|
87
|
+
['InvalidTokenError', new InvalidTokenError()],
|
|
88
|
+
['NotFoundError', new NotFoundError('Session not found')],
|
|
89
|
+
['ForbiddenError', new ForbiddenError()],
|
|
90
|
+
['UnauthorizedError', new UnauthorizedError()],
|
|
91
|
+
['NotSupportedError', new NotSupportedError()],
|
|
92
|
+
['BadRequestError', new BadRequestError()],
|
|
93
|
+
])('Unrecoverable Errors: should logout on %s during refresh', async (_, error) => {
|
|
94
|
+
setupServiceWithToken();
|
|
95
|
+
mockApiClient.refresh.mockRejectedValue(error);
|
|
86
96
|
// Wait for loop to pick up refresh
|
|
87
|
-
await timeout(
|
|
88
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
89
|
-
expect(service.token()).toBeUndefined();
|
|
90
|
-
});
|
|
91
|
-
test('Unrecoverable Errors: should logout on NotFoundError during refresh', async () => {
|
|
92
|
-
const now = Math.floor(Date.now() / 1000);
|
|
93
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
94
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
95
|
-
service = injector.resolve(AuthenticationClientService);
|
|
96
|
-
mockApiClient.refresh.mockRejectedValue(new NotFoundError('Session not found'));
|
|
97
|
-
await timeout(100);
|
|
98
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
99
|
-
expect(service.token()).toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
test('Unrecoverable Errors: should logout on ForbiddenError during refresh', async () => {
|
|
102
|
-
const now = Math.floor(Date.now() / 1000);
|
|
103
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
104
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
105
|
-
service = injector.resolve(AuthenticationClientService);
|
|
106
|
-
mockApiClient.refresh.mockRejectedValue(new ForbiddenError());
|
|
107
|
-
await timeout(100);
|
|
108
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
109
|
-
expect(service.token()).toBeUndefined();
|
|
110
|
-
});
|
|
111
|
-
test('Unrecoverable Errors: should logout on UnauthorizedError during refresh', async () => {
|
|
112
|
-
const now = Math.floor(Date.now() / 1000);
|
|
113
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
114
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
115
|
-
service = injector.resolve(AuthenticationClientService);
|
|
116
|
-
mockApiClient.refresh.mockRejectedValue(new UnauthorizedError());
|
|
117
|
-
await timeout(100);
|
|
118
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
119
|
-
expect(service.token()).toBeUndefined();
|
|
120
|
-
});
|
|
121
|
-
test('Unrecoverable Errors: should logout on NotSupportedError during refresh', async () => {
|
|
122
|
-
const now = Math.floor(Date.now() / 1000);
|
|
123
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
124
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
125
|
-
service = injector.resolve(AuthenticationClientService);
|
|
126
|
-
mockApiClient.refresh.mockRejectedValue(new NotSupportedError());
|
|
127
|
-
await timeout(100);
|
|
128
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
129
|
-
expect(service.token()).toBeUndefined();
|
|
130
|
-
});
|
|
131
|
-
test('Unrecoverable Errors: should logout on BadRequestError during refresh', async () => {
|
|
132
|
-
const now = Math.floor(Date.now() / 1000);
|
|
133
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
134
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
135
|
-
service = injector.resolve(AuthenticationClientService);
|
|
136
|
-
mockApiClient.refresh.mockRejectedValue(new BadRequestError());
|
|
137
|
-
await timeout(100);
|
|
97
|
+
await timeout(20);
|
|
138
98
|
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
139
99
|
expect(service.token()).toBeUndefined();
|
|
140
100
|
});
|
|
141
101
|
test('Recoverable Errors: should NOT logout on generic Error during refresh', async () => {
|
|
142
|
-
|
|
143
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
144
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
145
|
-
service = injector.resolve(AuthenticationClientService);
|
|
102
|
+
setupServiceWithToken();
|
|
146
103
|
mockApiClient.refresh.mockRejectedValue(new Error('Network failure'));
|
|
147
|
-
await timeout(
|
|
104
|
+
await timeout(20);
|
|
148
105
|
// Should NOT have called endSession (logout)
|
|
149
106
|
expect(mockApiClient.endSession).not.toHaveBeenCalled();
|
|
150
107
|
// Token should still be present (retry logic handles it)
|
|
151
108
|
expect(service.token()).toBeDefined();
|
|
109
|
+
// Trigger immediate retry to finish test quickly
|
|
110
|
+
service.requestRefresh();
|
|
111
|
+
await timeout(20);
|
|
152
112
|
});
|
|
153
113
|
test('Logout Failure: should clear local token even if server logout fails', async () => {
|
|
154
|
-
|
|
155
|
-
const initialToken = { exp: now + 1000, jti: 'initial' };
|
|
156
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
157
|
-
service = injector.resolve(AuthenticationClientService);
|
|
114
|
+
setupServiceWithToken(-1000, 1000);
|
|
158
115
|
mockApiClient.endSession.mockRejectedValue(new Error('Network error during logout'));
|
|
159
116
|
await service.logout();
|
|
160
117
|
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
@@ -65,59 +65,59 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
|
|
|
65
65
|
test('Zombie Timer: loop should wake up immediately when token changes', async () => {
|
|
66
66
|
// 1. Mock a long expiration
|
|
67
67
|
const now = Math.floor(Date.now() / 1000);
|
|
68
|
-
const initialToken = { exp: now + 3600, jti: 'initial' };
|
|
68
|
+
const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
|
|
69
69
|
// Set in storage so initialize() (called by resolve) loads it
|
|
70
70
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
71
71
|
service = injector.resolve(AuthenticationClientService);
|
|
72
72
|
// Wait for loop to enter the race condition (wait phase)
|
|
73
|
-
await timeout(
|
|
73
|
+
await timeout(20);
|
|
74
74
|
// 2. Change token
|
|
75
|
-
const newToken = { exp: now + 3600, jti: 'new' };
|
|
75
|
+
const newToken = { iat: now - 1800, exp: now + 3600, jti: 'new' };
|
|
76
76
|
mockApiClient.refresh.mockResolvedValue(newToken);
|
|
77
77
|
service.requestRefresh(); // This should trigger immediate wake up
|
|
78
78
|
// Wait for loop to process
|
|
79
|
-
await timeout(
|
|
79
|
+
await timeout(20);
|
|
80
80
|
expect(mockApiClient.refresh).toHaveBeenCalled();
|
|
81
81
|
});
|
|
82
82
|
test('Forced Refresh Loss: forceRefreshToken should not be cleared on failure', async () => {
|
|
83
83
|
const now = Math.floor(Date.now() / 1000);
|
|
84
|
-
const initialToken = { exp: now + 3600, jti: 'initial' };
|
|
84
|
+
const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
|
|
85
85
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
86
86
|
service = injector.resolve(AuthenticationClientService);
|
|
87
|
-
await timeout(
|
|
87
|
+
await timeout(20);
|
|
88
88
|
// 1. Mock refresh failure
|
|
89
89
|
mockApiClient.refresh.mockRejectedValue(new Error('Network Error'));
|
|
90
90
|
service.requestRefresh();
|
|
91
91
|
// Wait for loop to attempt refresh and fail
|
|
92
|
-
await timeout(
|
|
92
|
+
await timeout(50);
|
|
93
93
|
expect(mockApiClient.refresh).toHaveBeenCalled();
|
|
94
94
|
expect(service.forceRefreshToken.isSet).toBe(true); // Should STILL be set
|
|
95
95
|
});
|
|
96
96
|
test('Lock Contention Backoff: should wait 5 seconds and not busy-loop', async () => {
|
|
97
97
|
const now = Math.floor(Date.now() / 1000);
|
|
98
|
-
const initialToken = { exp: now + 5, jti: 'initial' }; // Expiring soon
|
|
98
|
+
const initialToken = { iat: now - 3600, exp: now + 5, jti: 'initial' }; // Expiring soon
|
|
99
99
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
100
100
|
// 1. Mock lock already held
|
|
101
101
|
mockLock.tryUse.mockResolvedValue(undefined); // lockAcquired = false
|
|
102
102
|
const startTime = Date.now();
|
|
103
103
|
service = injector.resolve(AuthenticationClientService);
|
|
104
|
-
// We expect it to try once, fail to get lock, and then wait
|
|
105
|
-
await timeout(
|
|
104
|
+
// We expect it to try once, fail to get lock, and then wait some time.
|
|
105
|
+
await timeout(50);
|
|
106
106
|
expect(mockLock.tryUse).toHaveBeenCalledTimes(1);
|
|
107
107
|
// Check if it's still waiting (not finished loop)
|
|
108
108
|
const duration = Date.now() - startTime;
|
|
109
|
-
expect(duration).toBeLessThan(
|
|
109
|
+
expect(duration).toBeLessThan(500);
|
|
110
110
|
});
|
|
111
111
|
test('Busy Loop: should not busy loop when forceRefreshToken is set and lock is held', async () => {
|
|
112
112
|
const now = Math.floor(Date.now() / 1000);
|
|
113
|
-
const initialToken = { exp: now + 3600, jti: 'initial' };
|
|
113
|
+
const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
|
|
114
114
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
115
115
|
// Mock lock already held
|
|
116
116
|
mockLock.tryUse.mockResolvedValue(undefined);
|
|
117
117
|
service = injector.resolve(AuthenticationClientService);
|
|
118
|
-
await timeout(
|
|
118
|
+
await timeout(20);
|
|
119
119
|
service.requestRefresh(); // Set the flag
|
|
120
|
-
await timeout(
|
|
120
|
+
await timeout(50);
|
|
121
121
|
// If it busy loops, this will be much higher than 1.
|
|
122
122
|
expect(mockLock.tryUse.mock.calls.length).toBeLessThan(5);
|
|
123
123
|
});
|
package/cancellation/token.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type ConnectConfig = {
|
|
|
46
46
|
* (emits when set).
|
|
47
47
|
*/
|
|
48
48
|
export declare class CancellationSignal implements PromiseLike<void>, Subscribable<void> {
|
|
49
|
+
#private;
|
|
49
50
|
protected readonly _stateSubject: BehaviorSubject<boolean>;
|
|
50
51
|
/**
|
|
51
52
|
* Observable which emits the current state (true for set, false for unset)
|
|
@@ -91,6 +92,11 @@ export declare class CancellationSignal implements PromiseLike<void>, Subscribab
|
|
|
91
92
|
* @internal
|
|
92
93
|
*/
|
|
93
94
|
constructor(stateSubject: BehaviorSubject<boolean>);
|
|
95
|
+
/**
|
|
96
|
+
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
97
|
+
* Useful for interoperability with APIs like `fetch`.
|
|
98
|
+
*/
|
|
99
|
+
get abortSignal(): AbortSignal;
|
|
94
100
|
/**
|
|
95
101
|
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
96
102
|
* Useful for interoperability with APIs like `fetch`.
|
package/cancellation/token.js
CHANGED
|
@@ -15,6 +15,7 @@ import { isBoolean } from '../utils/type-guards.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export class CancellationSignal {
|
|
17
17
|
_stateSubject;
|
|
18
|
+
#abortSignal;
|
|
18
19
|
/**
|
|
19
20
|
* Observable which emits the current state (true for set, false for unset)
|
|
20
21
|
* and any subsequent state changes.
|
|
@@ -74,6 +75,13 @@ export class CancellationSignal {
|
|
|
74
75
|
constructor(stateSubject) {
|
|
75
76
|
this._stateSubject = stateSubject;
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
80
|
+
* Useful for interoperability with APIs like `fetch`.
|
|
81
|
+
*/
|
|
82
|
+
get abortSignal() {
|
|
83
|
+
return (this.#abortSignal ??= this.asAbortSignal());
|
|
84
|
+
}
|
|
77
85
|
/**
|
|
78
86
|
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
79
87
|
* Useful for interoperability with APIs like `fetch`.
|
|
@@ -20,7 +20,6 @@ export class UndiciHttpClientAdapterOptions {
|
|
|
20
20
|
;
|
|
21
21
|
let UndiciHttpClientAdapter = class UndiciHttpClientAdapter extends HttpClientAdapter {
|
|
22
22
|
options = inject(UndiciHttpClientAdapterOptions, undefined, { optional: true }) ?? {};
|
|
23
|
-
// eslint-disable-next-line max-lines-per-function, max-statements
|
|
24
23
|
async call(httpClientRequest) {
|
|
25
24
|
let body;
|
|
26
25
|
if (isDefined(httpClientRequest.body?.json)) {
|
|
@@ -64,7 +63,6 @@ let UndiciHttpClientAdapter = class UndiciHttpClientAdapter extends HttpClientAd
|
|
|
64
63
|
const httpClientResponse = new HttpClientResponse({
|
|
65
64
|
request: httpClientRequest,
|
|
66
65
|
statusCode: response.statusCode,
|
|
67
|
-
statusMessage: '?',
|
|
68
66
|
headers: new HttpHeaders(response.headers),
|
|
69
67
|
body: response.body,
|
|
70
68
|
closeHandler: () => response.body.destroy(),
|
|
@@ -34,6 +34,7 @@ export type HttpClientRequestOptions = Partial<TypedOmit<HttpClientRequest, 'url
|
|
|
34
34
|
formData?: HttpFormDataObject | FormData;
|
|
35
35
|
};
|
|
36
36
|
abortSignal?: CancellationSignal;
|
|
37
|
+
priority?: RequestPriority;
|
|
37
38
|
};
|
|
38
39
|
export type HttpRequestCredentials = 'omit' | 'same-origin' | 'include';
|
|
39
40
|
export type HttpClientRequestObject = HttpClientRequestOptions & {
|
|
@@ -95,6 +96,7 @@ export declare class HttpClientRequest implements Disposable {
|
|
|
95
96
|
credentials: HttpRequestCredentials | undefined;
|
|
96
97
|
authorization: HttpRequestAuthorization | undefined;
|
|
97
98
|
body: HttpRequestBody | undefined;
|
|
99
|
+
priority: RequestPriority | undefined;
|
|
98
100
|
/**
|
|
99
101
|
* Request timeout in milliseconds
|
|
100
102
|
* @default 30000
|
|
@@ -61,6 +61,7 @@ export class HttpClientRequest {
|
|
|
61
61
|
credentials;
|
|
62
62
|
authorization;
|
|
63
63
|
body;
|
|
64
|
+
priority;
|
|
64
65
|
/**
|
|
65
66
|
* Request timeout in milliseconds
|
|
66
67
|
* @default 30000
|
|
@@ -101,6 +102,7 @@ export class HttpClientRequest {
|
|
|
101
102
|
this.authorization = requestOptions.authorization;
|
|
102
103
|
this.body = normalizeBody(requestOptions.body);
|
|
103
104
|
this.credentials = requestOptions.credentials;
|
|
105
|
+
this.priority = requestOptions.priority;
|
|
104
106
|
this.timeout = requestOptions.timeout ?? 30000;
|
|
105
107
|
this.throwOnNon200 = requestOptions.throwOnNon200 ?? true;
|
|
106
108
|
this.context = requestOptions.context ?? {};
|
|
@@ -122,6 +124,7 @@ export class HttpClientRequest {
|
|
|
122
124
|
request.authorization = clone(request.authorization, true);
|
|
123
125
|
request.urlParameters = new HttpUrlParameters(request.urlParameters);
|
|
124
126
|
request.body = normalizeBody(request.body);
|
|
127
|
+
request.priority = this.priority;
|
|
125
128
|
return request;
|
|
126
129
|
}
|
|
127
130
|
asObject() {
|
|
@@ -135,6 +138,7 @@ export class HttpClientRequest {
|
|
|
135
138
|
query: this.query.asObject(),
|
|
136
139
|
authorization: this.authorization,
|
|
137
140
|
body,
|
|
141
|
+
priority: this.priority,
|
|
138
142
|
timeout: this.timeout,
|
|
139
143
|
throwOnNon200: this.throwOnNon200,
|
|
140
144
|
context: this.context,
|
|
@@ -9,7 +9,7 @@ export type HttpClientResponseObject = TypedOmit<HttpClientResponse, 'hasBody' |
|
|
|
9
9
|
export type HttpClientResponseOptions = {
|
|
10
10
|
request: HttpClientRequest;
|
|
11
11
|
statusCode: number;
|
|
12
|
-
statusMessage
|
|
12
|
+
statusMessage?: string | null;
|
|
13
13
|
headers: HttpHeadersObject | HttpHeaders;
|
|
14
14
|
body: HttpBody | HttpBodySource;
|
|
15
15
|
closeHandler: () => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isString } from '../../utils/type-guards.js';
|
|
2
2
|
import { HttpBody } from '../http-body.js';
|
|
3
3
|
import { HttpHeaders } from '../http-headers.js';
|
|
4
|
+
import { getHttpStatusMessage } from '../utils.js';
|
|
4
5
|
export class HttpClientResponse {
|
|
5
6
|
closeHandler;
|
|
6
7
|
request;
|
|
@@ -14,7 +15,7 @@ export class HttpClientResponse {
|
|
|
14
15
|
constructor(options) {
|
|
15
16
|
this.request = options.request;
|
|
16
17
|
this.statusCode = options.statusCode;
|
|
17
|
-
this.statusMessage = (isString(options.statusMessage) && (options.statusMessage.length > 0)) ? options.statusMessage :
|
|
18
|
+
this.statusMessage = (isString(options.statusMessage) && (options.statusMessage.length > 0)) ? options.statusMessage : getHttpStatusMessage(this.statusCode);
|
|
18
19
|
this.headers = new HttpHeaders(options.headers);
|
|
19
20
|
this.body = (options.body instanceof HttpBody) ? options.body : new HttpBody(options.body, this.headers);
|
|
20
21
|
this.closeHandler = options.closeHandler;
|
|
@@ -28,7 +29,7 @@ export class HttpClientResponse {
|
|
|
28
29
|
statusCode: this.statusCode,
|
|
29
30
|
statusMessage: this.statusMessage,
|
|
30
31
|
headers: this.headers.asObject(),
|
|
31
|
-
body: this.body
|
|
32
|
+
body: this.body,
|
|
32
33
|
};
|
|
33
34
|
return obj;
|
|
34
35
|
}
|
package/http/utils.d.ts
CHANGED
|
@@ -15,4 +15,10 @@ export declare function readBodyAsTextStream(body: Body, headers: HttpHeaders, o
|
|
|
15
15
|
export declare function readBodyAsJson(body: Body, headers: HttpHeaders, options?: ReadBodyAsJsonOptions): Promise<UndefinableJson>;
|
|
16
16
|
export declare function readBody(body: Body, headers: HttpHeaders, options?: ReadBodyOptions): Promise<string | UndefinableJson | Uint8Array>;
|
|
17
17
|
export declare function readBodyAsStream(body: Body, headers: HttpHeaders, options?: ReadBodyOptions): ReadableStream<string> | ReadableStream<Uint8Array>;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the standard HTTP status message for a given status code.
|
|
20
|
+
* @param statusCode The HTTP status code.
|
|
21
|
+
* @returns The standard status message, or 'Unknown' if the code is not recognized.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getHttpStatusMessage(statusCode: number): string;
|
|
18
24
|
export {};
|
package/http/utils.js
CHANGED
|
@@ -104,3 +104,74 @@ function ensureSize(length, options) {
|
|
|
104
104
|
throw MaxBytesExceededError.fromBytes(options.maxBytes);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
const httpStatusMessages = {
|
|
108
|
+
100: 'Continue',
|
|
109
|
+
101: 'Switching Protocols',
|
|
110
|
+
102: 'Processing',
|
|
111
|
+
103: 'Early Hints',
|
|
112
|
+
200: 'OK',
|
|
113
|
+
201: 'Created',
|
|
114
|
+
202: 'Accepted',
|
|
115
|
+
203: 'Non-Authoritative Information',
|
|
116
|
+
204: 'No Content',
|
|
117
|
+
205: 'Reset Content',
|
|
118
|
+
206: 'Partial Content',
|
|
119
|
+
207: 'Multi-Status',
|
|
120
|
+
208: 'Already Reported',
|
|
121
|
+
226: 'IM Used',
|
|
122
|
+
300: 'Multiple Choices',
|
|
123
|
+
301: 'Moved Permanently',
|
|
124
|
+
302: 'Found',
|
|
125
|
+
303: 'See Other',
|
|
126
|
+
304: 'Not Modified',
|
|
127
|
+
307: 'Temporary Redirect',
|
|
128
|
+
308: 'Permanent Redirect',
|
|
129
|
+
400: 'Bad Request',
|
|
130
|
+
401: 'Unauthorized',
|
|
131
|
+
402: 'Payment Required',
|
|
132
|
+
403: 'Forbidden',
|
|
133
|
+
404: 'Not Found',
|
|
134
|
+
405: 'Method Not Allowed',
|
|
135
|
+
406: 'Not Acceptable',
|
|
136
|
+
407: 'Proxy Authentication Required',
|
|
137
|
+
408: 'Request Timeout',
|
|
138
|
+
409: 'Conflict',
|
|
139
|
+
410: 'Gone',
|
|
140
|
+
411: 'Length Required',
|
|
141
|
+
412: 'Precondition Failed',
|
|
142
|
+
413: 'Content Too Large',
|
|
143
|
+
414: 'URI Too Long',
|
|
144
|
+
415: 'Unsupported Media Type',
|
|
145
|
+
416: 'Range Not Satisfiable',
|
|
146
|
+
417: 'Expectation Failed',
|
|
147
|
+
418: 'I\'m a teapot',
|
|
148
|
+
421: 'Misdirected Request',
|
|
149
|
+
422: 'Unprocessable Content',
|
|
150
|
+
423: 'Locked',
|
|
151
|
+
424: 'Failed Dependency',
|
|
152
|
+
425: 'Too Early',
|
|
153
|
+
426: 'Upgrade Required',
|
|
154
|
+
428: 'Precondition Required',
|
|
155
|
+
429: 'Too Many Requests',
|
|
156
|
+
431: 'Request Header Fields Too Large',
|
|
157
|
+
451: 'Unavailable For Legal Reasons',
|
|
158
|
+
500: 'Internal Server Error',
|
|
159
|
+
501: 'Not Implemented',
|
|
160
|
+
502: 'Bad Gateway',
|
|
161
|
+
503: 'Service Unavailable',
|
|
162
|
+
504: 'Gateway Timeout',
|
|
163
|
+
505: 'HTTP Version Not Supported',
|
|
164
|
+
506: 'Variant Also Negotiates',
|
|
165
|
+
507: 'Insufficient Storage',
|
|
166
|
+
508: 'Loop Detected',
|
|
167
|
+
510: 'Not Extended',
|
|
168
|
+
511: 'Network Authentication Required',
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Returns the standard HTTP status message for a given status code.
|
|
172
|
+
* @param statusCode The HTTP status code.
|
|
173
|
+
* @returns The standard status message, or 'Unknown' if the code is not recognized.
|
|
174
|
+
*/
|
|
175
|
+
export function getHttpStatusMessage(statusCode) {
|
|
176
|
+
return httpStatusMessages[statusCode] ?? `Unknown (${statusCode})`;
|
|
177
|
+
}
|
package/injector/graph.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toArray } from '../utils/array/array.js';
|
|
2
2
|
import { isArray, isDefined, isFunction } from '../utils/type-guards.js';
|
|
3
|
+
import { typeOf } from '../utils/type-of.js';
|
|
3
4
|
import { Injector } from './injector.js';
|
|
4
5
|
import { afterResolve } from './interfaces.js';
|
|
5
6
|
import { isClassProvider, isFactoryProvider, isProviderWithInitializer, isTokenProvider, isValueProvider } from './provider.js';
|
|
@@ -185,8 +186,8 @@ export function getDependencyGraph(injector, options = {}) {
|
|
|
185
186
|
label += ' (multi)';
|
|
186
187
|
}
|
|
187
188
|
if (isDefined(node.argument)) {
|
|
188
|
-
const s = JSON.stringify(node.argument);
|
|
189
|
-
label += `\narg: ${s.length > 20 ? `${s.
|
|
189
|
+
const s = isFunction(node.argument) ? typeOf(node.argument) : JSON.stringify(node.argument) ?? 'Could not stringify argument';
|
|
190
|
+
label += `\narg: ${(s.length > 20) ? `${s.slice(0, 17)}...` : s}`;
|
|
190
191
|
}
|
|
191
192
|
if (node.isCached) {
|
|
192
193
|
label += '\n(cached)';
|
|
@@ -281,6 +282,7 @@ export function renderDependencyGraphToDot(graph, options = {}) {
|
|
|
281
282
|
return tokenToId.get(token);
|
|
282
283
|
};
|
|
283
284
|
const lines = ['strict digraph G {'];
|
|
285
|
+
// --- Graph Global Attributes ---
|
|
284
286
|
if (bgcolor) {
|
|
285
287
|
lines.push(` bgcolor=${JSON.stringify(bgcolor)};`);
|
|
286
288
|
}
|
|
@@ -293,6 +295,7 @@ export function renderDependencyGraphToDot(graph, options = {}) {
|
|
|
293
295
|
lines.push(` node [shape=box, fontname=${JSON.stringify(fontname)}, style=filled, fillcolor="#f9f9f9", fontsize=${fontsize}];`);
|
|
294
296
|
lines.push(` edge [fontname=${JSON.stringify(fontname)}, fontsize=${fontsize - 1}];`);
|
|
295
297
|
lines.push(` rankdir=${rankdir}; compound=true;`);
|
|
298
|
+
// --- 1. Render Edges ---
|
|
296
299
|
for (const edge of graph.edges) {
|
|
297
300
|
const color = edge.isCycle ? '#e74c3c' : edge.color;
|
|
298
301
|
const penwidth = edge.isCycle ? 2.0 : edge.penwidth;
|
|
@@ -304,18 +307,30 @@ export function renderDependencyGraphToDot(graph, options = {}) {
|
|
|
304
307
|
].filter(Boolean).join(', ');
|
|
305
308
|
lines.push(` ${getTokenId(edge.from)} -> ${getTokenId(edge.to)} [${attributes}];`);
|
|
306
309
|
}
|
|
310
|
+
// --- Helper: Render Node Definition ---
|
|
307
311
|
const renderNode = (node) => {
|
|
308
312
|
const metadata = node.metadata;
|
|
309
313
|
const lifecycle = metadata.lifecycle;
|
|
310
314
|
const first = lifecycle.split('|')[0];
|
|
311
|
-
const colors = {
|
|
312
|
-
|
|
315
|
+
const colors = {
|
|
316
|
+
singleton: '#d1e7dd',
|
|
317
|
+
injector: '#fff3cd',
|
|
318
|
+
resolution: '#cfe2ff',
|
|
319
|
+
transient: '#f8d7da',
|
|
320
|
+
};
|
|
321
|
+
const shapes = {
|
|
322
|
+
singleton: 'doubleoctagon',
|
|
323
|
+
injector: 'component',
|
|
324
|
+
resolution: 'ellipse',
|
|
325
|
+
transient: 'box',
|
|
326
|
+
};
|
|
313
327
|
const name = node.name + (metadata.hasAfterResolve ? ' *' : '');
|
|
314
328
|
const label = showMetadata ? `${name}\n[${lifecycle}, ${metadata.type}]` : name;
|
|
315
329
|
const fillcolor = colors[first] ?? '#f9f9f9';
|
|
316
330
|
const shape = shapes[first] ?? 'box';
|
|
317
331
|
return ` ${getTokenId(node.token)} [label=${JSON.stringify(label)}, fillcolor=${JSON.stringify(fillcolor)}, shape=${JSON.stringify(shape)}];`;
|
|
318
332
|
};
|
|
333
|
+
// --- 2. Render Injector Clusters and Nodes ---
|
|
319
334
|
const injectors = groupByInjector ? Array.from(graph.injectorNames.keys()) : [0];
|
|
320
335
|
for (const injectorId of injectors) {
|
|
321
336
|
if (groupByInjector) {
|
|
@@ -330,11 +345,14 @@ export function renderDependencyGraphToDot(graph, options = {}) {
|
|
|
330
345
|
if (!graph.nodes.has(owner)) {
|
|
331
346
|
continue;
|
|
332
347
|
}
|
|
333
|
-
const subNodes = Array.from(tokens)
|
|
348
|
+
const subNodes = Array.from(tokens)
|
|
349
|
+
.filter((token) => graph.nodes.has(token) && (!groupByInjector || graph.nodes.get(token).injectorId == injectorId))
|
|
350
|
+
.map((token) => graph.nodes.get(token));
|
|
334
351
|
if (subNodes.length == 0) {
|
|
335
352
|
continue;
|
|
336
353
|
}
|
|
337
|
-
|
|
354
|
+
const ownerName = graph.nodes.get(owner).name;
|
|
355
|
+
lines.push(` subgraph cluster_${injectorId}_${getTokenId(owner)} { label = ${JSON.stringify(`Dynamic Resolutions of ${ownerName}`)}; style = dashed; color = "#9b59b6"; fontname = ${JSON.stringify(fontname)}; fontsize = ${fontsize};`);
|
|
338
356
|
for (const node of subNodes) {
|
|
339
357
|
lines.push(renderNode(node));
|
|
340
358
|
inDynamic.add(node.token);
|
|
@@ -351,6 +369,7 @@ export function renderDependencyGraphToDot(graph, options = {}) {
|
|
|
351
369
|
lines.push(' }');
|
|
352
370
|
}
|
|
353
371
|
}
|
|
372
|
+
// --- 3. Render Legend ---
|
|
354
373
|
if (showLegend) {
|
|
355
374
|
const legendFontSize = fontsize - 1;
|
|
356
375
|
lines.push(` subgraph cluster_legend { label = "Legend"; style = solid; color = gray; fontname = ${JSON.stringify(`${fontname}-Bold`)}; fontsize = ${fontsize + 1};`);
|
|
@@ -404,7 +423,9 @@ export function renderDependencyGraphToDot(graph, options = {}) {
|
|
|
404
423
|
lines.push(' l_circular_from -> l_cached_from -> l_optional_from -> l_cross_from [style=invis];');
|
|
405
424
|
lines.push(' l_circular_to -> l_cached_to -> l_optional_to -> l_cross_to [style=invis];');
|
|
406
425
|
lines.push(' l_alias_from -> l_forward_from -> l_dynamic_from -> l_spacer_from [style=invis];');
|
|
426
|
+
lines.push(' }');
|
|
407
427
|
}
|
|
428
|
+
lines.push('}');
|
|
408
429
|
return lines.join('\n');
|
|
409
430
|
}
|
|
410
431
|
/**
|
package/injector/injector.js
CHANGED
|
@@ -7,6 +7,7 @@ import { DeferredPromise } from '../promise/deferred-promise.js';
|
|
|
7
7
|
import { reflectionRegistry } from '../reflection/registry.js';
|
|
8
8
|
import { toArray } from '../utils/array/array.js';
|
|
9
9
|
import { FactoryMap } from '../utils/factory-map.js';
|
|
10
|
+
import { noop } from '../utils/noop.js';
|
|
10
11
|
import { ForwardRef } from '../utils/object/forward-ref.js';
|
|
11
12
|
import { objectEntries } from '../utils/object/object.js';
|
|
12
13
|
import { assert, isArray, isDefined, isFunction, isNotNull, isNotObject, isNull, isPromise, isUndefined } from '../utils/type-guards.js';
|
|
@@ -597,6 +598,7 @@ function newInternalResolveContext(injector) {
|
|
|
597
598
|
forwardRefs: new Set(),
|
|
598
599
|
$done: new DeferredPromise(),
|
|
599
600
|
};
|
|
601
|
+
void context.$done.catch(noop); // prevent unhandled rejection if no one is awaiting it
|
|
600
602
|
return context;
|
|
601
603
|
}
|
|
602
604
|
function postProcess(context) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "0c48afa4-9ab0-4965-a93e-05a6c1b88e58",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -44,37 +44,6 @@
|
|
|
44
44
|
"type": "text[]",
|
|
45
45
|
"primaryKey": false,
|
|
46
46
|
"notNull": false
|
|
47
|
-
},
|
|
48
|
-
"revision": {
|
|
49
|
-
"name": "revision",
|
|
50
|
-
"type": "integer",
|
|
51
|
-
"primaryKey": false,
|
|
52
|
-
"notNull": true
|
|
53
|
-
},
|
|
54
|
-
"revision_timestamp": {
|
|
55
|
-
"name": "revision_timestamp",
|
|
56
|
-
"type": "timestamp with time zone",
|
|
57
|
-
"primaryKey": false,
|
|
58
|
-
"notNull": true
|
|
59
|
-
},
|
|
60
|
-
"create_timestamp": {
|
|
61
|
-
"name": "create_timestamp",
|
|
62
|
-
"type": "timestamp with time zone",
|
|
63
|
-
"primaryKey": false,
|
|
64
|
-
"notNull": true
|
|
65
|
-
},
|
|
66
|
-
"delete_timestamp": {
|
|
67
|
-
"name": "delete_timestamp",
|
|
68
|
-
"type": "timestamp with time zone",
|
|
69
|
-
"primaryKey": false,
|
|
70
|
-
"notNull": false
|
|
71
|
-
},
|
|
72
|
-
"attributes": {
|
|
73
|
-
"name": "attributes",
|
|
74
|
-
"type": "jsonb",
|
|
75
|
-
"primaryKey": false,
|
|
76
|
-
"notNull": true,
|
|
77
|
-
"default": "'{}'::jsonb"
|
|
78
47
|
}
|
|
79
48
|
},
|
|
80
49
|
"indexes": {},
|
|
@@ -5,15 +5,8 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "7",
|
|
8
|
-
"when":
|
|
9
|
-
"tag": "
|
|
10
|
-
"breakpoints": true
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"idx": 1,
|
|
14
|
-
"version": "7",
|
|
15
|
-
"when": 1761124592125,
|
|
16
|
-
"tag": "0001_flimsy_bloodscream",
|
|
8
|
+
"when": 1771240070681,
|
|
9
|
+
"tag": "0000_numerous_the_watchers",
|
|
17
10
|
"breakpoints": true
|
|
18
11
|
}
|
|
19
12
|
]
|