@tstdl/base 0.93.127 → 0.93.129

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.
Files changed (57) hide show
  1. package/api/client/client.js +45 -9
  2. package/api/client/tests/api-client.test.d.ts +1 -0
  3. package/api/client/tests/api-client.test.js +194 -0
  4. package/api/types.d.ts +35 -3
  5. package/authentication/client/authentication.service.js +30 -11
  6. package/authentication/client/http-client.middleware.js +10 -3
  7. package/authentication/server/authentication.service.d.ts +12 -0
  8. package/authentication/server/authentication.service.js +14 -2
  9. package/authentication/tests/authentication.client-error-handling.test.js +23 -66
  10. package/authentication/tests/authentication.client-service-refresh.test.js +14 -14
  11. package/cancellation/token.d.ts +6 -0
  12. package/cancellation/token.js +8 -0
  13. package/document-management/server/services/document-file.service.js +10 -9
  14. package/document-management/server/services/document-management-ancillary.service.d.ts +12 -1
  15. package/document-management/server/services/document-management-ancillary.service.js +9 -0
  16. package/file/server/temporary-file.d.ts +2 -1
  17. package/file/server/temporary-file.js +5 -1
  18. package/http/client/adapters/undici.adapter.js +0 -2
  19. package/http/client/http-client-request.d.ts +2 -0
  20. package/http/client/http-client-request.js +4 -0
  21. package/http/client/http-client-response.d.ts +1 -1
  22. package/http/client/http-client-response.js +3 -2
  23. package/http/utils.d.ts +6 -0
  24. package/http/utils.js +71 -0
  25. package/injector/injector.js +2 -0
  26. package/mail/drizzle/0000_numerous_the_watchers.sql +8 -0
  27. package/mail/drizzle/meta/0000_snapshot.json +1 -32
  28. package/mail/drizzle/meta/_journal.json +2 -9
  29. package/object-storage/s3/s3.object-storage.js +6 -6
  30. package/object-storage/s3/tests/s3.object-storage.integration.test.js +22 -53
  31. package/orm/tests/repository-expiration.test.js +3 -3
  32. package/package.json +1 -1
  33. package/pdf/utils.d.ts +24 -3
  34. package/pdf/utils.js +89 -30
  35. package/process/spawn.d.ts +1 -1
  36. package/rate-limit/tests/postgres-rate-limiter.test.js +9 -7
  37. package/renderer/typst.d.ts +5 -0
  38. package/renderer/typst.js +9 -5
  39. package/task-queue/tests/complex.test.js +22 -22
  40. package/task-queue/tests/dependencies.test.js +15 -13
  41. package/task-queue/tests/queue.test.js +13 -13
  42. package/task-queue/tests/worker.test.js +12 -12
  43. package/testing/integration-setup.d.ts +2 -0
  44. package/testing/integration-setup.js +13 -7
  45. package/utils/backoff.d.ts +27 -3
  46. package/utils/backoff.js +31 -9
  47. package/utils/index.d.ts +1 -0
  48. package/utils/index.js +1 -0
  49. package/utils/retry-with-backoff.d.ts +22 -0
  50. package/utils/retry-with-backoff.js +64 -0
  51. package/utils/tests/backoff.test.d.ts +1 -0
  52. package/utils/tests/backoff.test.js +41 -0
  53. package/utils/tests/retry-with-backoff.test.d.ts +1 -0
  54. package/utils/tests/retry-with-backoff.test.js +49 -0
  55. package/mail/drizzle/0000_previous_malcolm_colcord.sql +0 -13
  56. package/mail/drizzle/0001_flimsy_bloodscream.sql +0 -5
  57. 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('Unrecoverable Errors: should logout on InvalidTokenError during refresh', async () => {
81
- const now = Math.floor(Date.now() / 1000);
82
- const initialToken = { exp: now + 5, jti: 'initial' };
83
- globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
84
- service = injector.resolve(AuthenticationClientService);
85
- mockApiClient.refresh.mockRejectedValue(new InvalidTokenError());
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(100);
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
- const now = Math.floor(Date.now() / 1000);
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(100);
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
- const now = Math.floor(Date.now() / 1000);
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(100);
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(100);
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(100);
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(200);
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 5 seconds.
105
- await timeout(300);
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(1000);
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(100);
118
+ await timeout(20);
119
119
  service.requestRefresh(); // Set the flag
120
- await timeout(300);
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
  });
@@ -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`.
@@ -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`.
@@ -6,7 +6,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import sharp, {} from 'sharp';
8
8
  import { match } from 'ts-pattern';
9
- import { injectGenkit } from '../../../ai/genkit/index.js';
10
9
  import { ForbiddenError } from '../../../errors/forbidden.error.js';
11
10
  import { NotImplementedError } from '../../../errors/not-implemented.error.js';
12
11
  import { getMimeType, getMimeTypeExtensions, mimeTypes } from '../../../file/index.js';
@@ -20,14 +19,15 @@ import { digest } from '../../../utils/cryptography.js';
20
19
  import { currentTimestamp } from '../../../utils/date-time.js';
21
20
  import { getRandomString } from '../../../utils/random.js';
22
21
  import { readableStreamFromPromise, readBinaryStream } from '../../../utils/stream/index.js';
23
- import { isNotReadableStream, isNotUint8Array, isUint8Array } from '../../../utils/type-guards.js';
22
+ import { isNotReadableStream, isNotUint8Array, isString, isUint8Array } from '../../../utils/type-guards.js';
24
23
  import { millisecondsPerMinute } from '../../../utils/units.js';
25
24
  import { Document } from '../../models/index.js';
26
25
  import { DocumentManagementConfiguration } from '../module.js';
26
+ import { DocumentManagementAncillaryService } from './document-management-ancillary.service.js';
27
27
  import { DocumentManagementSingleton } from './singleton.js';
28
28
  let DocumentFileService = class DocumentFileService extends Transactional {
29
29
  #configuration = inject(DocumentManagementConfiguration);
30
- #genkit = injectGenkit();
30
+ #documentManagementAncillaryService = inject(DocumentManagementAncillaryService);
31
31
  #fileObjectStorage = inject(ObjectStorage, this.#configuration.fileObjectStorageModule);
32
32
  #filePreviewObjectStorage = inject(ObjectStorage, this.#configuration.filePreviewObjectStorageModule);
33
33
  #fileUploadObjectStorage = inject(ObjectStorage, { module: this.#configuration.fileUploadObjectStorageModule /* , configuration: { lifecycle: { expiration: { after: 5 * secondsPerMinute } } } */ });
@@ -86,7 +86,10 @@ let DocumentFileService = class DocumentFileService extends Transactional {
86
86
  return this.#fileObjectStorage.getContentStream(objectKey);
87
87
  }
88
88
  async getContentUrl(document, download = false) {
89
- return await this.getDocumentFileContentObjectUrl(document, document.title ?? document.id, download);
89
+ const resolvedFilename = await this.#documentManagementAncillaryService.getDocumentFilename(document);
90
+ const fileExtension = getMimeTypeExtensions(document.mimeType)[0] ?? 'bin';
91
+ const filename = isString(resolvedFilename) ? `${resolvedFilename}.${fileExtension}` : resolvedFilename.fullname;
92
+ return await this.getDocumentFileContentObjectUrl(document, filename, download);
90
93
  }
91
94
  /** Gets the underlying object storage object for the document file */
92
95
  async getObject(document) {
@@ -132,14 +135,12 @@ let DocumentFileService = class DocumentFileService extends Transactional {
132
135
  await this.#filePreviewObjectStorage.uploadObject(key, image, { contentLength: image.length, contentType: 'image/webp' });
133
136
  }
134
137
  }
135
- async getDocumentFileContentObjectUrl(document, title, download) {
138
+ async getDocumentFileContentObjectUrl(document, filename, download) {
136
139
  const key = getObjectKey(document.id);
137
- const fileExtension = getMimeTypeExtensions(document.mimeType)[0] ?? 'bin';
138
140
  const disposition = download ? 'attachment' : 'inline';
139
- const filename = `${title}.${fileExtension}`;
140
141
  return await this.#fileObjectStorage.getDownloadUrl(key, currentTimestamp() + (5 * millisecondsPerMinute), {
141
- 'Response-Content-Type': document.mimeType,
142
- 'Response-Content-Disposition': `${disposition}; filename = "${encodeURIComponent(filename)}"`,
142
+ 'Content-Type': document.mimeType,
143
+ 'Content-Disposition': `${disposition}; filename = "${encodeURIComponent(filename)}"`,
143
144
  });
144
145
  }
145
146
  };
@@ -1,5 +1,9 @@
1
- import type { DocumentCollection } from '../../models/index.js';
1
+ import type { Document, DocumentCollection } from '../../models/index.js';
2
2
  import type { DocumentCollectionMetadata } from '../../service-models/index.js';
3
+ export type DocumentFileNameResult = string | {
4
+ /** The full name of the document, including extension. */
5
+ fullname: string;
6
+ };
3
7
  export declare abstract class DocumentManagementAncillaryService {
4
8
  /**
5
9
  * Resolves application-specific metadata for a list of document collections.
@@ -7,4 +11,11 @@ export declare abstract class DocumentManagementAncillaryService {
7
11
  * @returns A promise that resolves to an array of DocumentCollectionMetadata, corresponding to the input collections.
8
12
  */
9
13
  abstract resolveMetadata(tenantId: string, collections: DocumentCollection[]): DocumentCollectionMetadata[] | Promise<DocumentCollectionMetadata[]>;
14
+ /**
15
+ * Resolve the file name for a document, which will be used when downloading the document file. The file extension is automatically determined based on the document's MIME type, so the returned file name should not include the extension.
16
+ * @param document The Document entity for which to resolve the file name.
17
+ * @param title An optional title provided by the api call.
18
+ * @returns The resolved file name for the document, without the file extension.
19
+ */
20
+ getDocumentFilename(document: Document): DocumentFileNameResult | Promise<DocumentFileNameResult>;
10
21
  }
@@ -1,2 +1,11 @@
1
1
  export class DocumentManagementAncillaryService {
2
+ /**
3
+ * Resolve the file name for a document, which will be used when downloading the document file. The file extension is automatically determined based on the document's MIME type, so the returned file name should not include the extension.
4
+ * @param document The Document entity for which to resolve the file name.
5
+ * @param title An optional title provided by the api call.
6
+ * @returns The resolved file name for the document, without the file extension.
7
+ */
8
+ getDocumentFilename(document) {
9
+ return document.title ?? document.originalFileName ?? document.id;
10
+ }
2
11
  }
@@ -12,7 +12,7 @@ export declare class TemporaryFile implements AsyncDisposable {
12
12
  /**
13
13
  * Prevents the temporary file from being deleted on disposal.
14
14
  */
15
- keep(): void;
15
+ keep(): this;
16
16
  read(): Promise<Uint8Array>;
17
17
  readText(): Promise<string>;
18
18
  readStream(): ReadableStream<Uint8Array>;
@@ -25,5 +25,6 @@ export declare class TemporaryFile implements AsyncDisposable {
25
25
  moveTo(path: string, keep?: boolean): Promise<void>;
26
26
  delete(): Promise<void>;
27
27
  size(): Promise<number>;
28
+ dispose(): Promise<void>;
28
29
  [Symbol.asyncDispose](): Promise<void>;
29
30
  }
@@ -36,6 +36,7 @@ export class TemporaryFile {
36
36
  */
37
37
  keep() {
38
38
  this.#keep = true;
39
+ return this;
39
40
  }
40
41
  async read() {
41
42
  return await readFile(this.#path);
@@ -69,7 +70,7 @@ export class TemporaryFile {
69
70
  const result = await stat(this.#path);
70
71
  return result.size;
71
72
  }
72
- async [Symbol.asyncDispose]() {
73
+ async dispose() {
73
74
  if (this.#keep) {
74
75
  return;
75
76
  }
@@ -78,4 +79,7 @@ export class TemporaryFile {
78
79
  }
79
80
  catch { }
80
81
  }
82
+ async [Symbol.asyncDispose]() {
83
+ await this.dispose();
84
+ }
81
85
  }
@@ -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: string | null;
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 : null;
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
+ }
@@ -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) {
@@ -0,0 +1,8 @@
1
+ CREATE TABLE "mail"."log" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "timestamp" timestamp with time zone NOT NULL,
4
+ "template" text,
5
+ "data" jsonb NOT NULL,
6
+ "send_result" jsonb,
7
+ "errors" text[]
8
+ );
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "f8cdba37-11b9-477a-9f5a-5ef4b5026011",
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": 1740059198387,
9
- "tag": "0000_previous_malcolm_colcord",
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
  ]