@tstdl/base 0.93.145 → 0.93.147
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/authentication/tests/authentication.client-service.test.js +15 -19
- package/authentication/tests/authentication.service.test.js +92 -119
- package/notification/tests/notification-client.test.js +39 -50
- package/notification/tests/notification-flow.test.js +204 -238
- package/notification/tests/notification-sse.service.test.js +20 -27
- package/notification/tests/notification-type.service.test.js +17 -20
- package/orm/repository.types.d.ts +13 -2
- package/orm/server/repository.d.ts +60 -4
- package/orm/server/repository.js +126 -25
- package/orm/tests/query-complex.test.js +80 -111
- package/orm/tests/repository-advanced.test.js +100 -143
- package/orm/tests/repository-attributes.test.js +30 -39
- package/orm/tests/repository-compound-primary-key.test.js +67 -75
- package/orm/tests/repository-comprehensive.test.js +76 -101
- package/orm/tests/repository-coverage.test.d.ts +1 -0
- package/orm/tests/repository-coverage.test.js +88 -149
- package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
- package/orm/tests/repository-cti-extensive.test.js +118 -147
- package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
- package/orm/tests/repository-cti-mapping.test.js +29 -42
- package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-cti-soft-delete.test.js +25 -37
- package/orm/tests/repository-cti-transactions.test.js +19 -33
- package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
- package/orm/tests/repository-cti-upsert-many.test.js +38 -50
- package/orm/tests/repository-cti.test.d.ts +1 -0
- package/orm/tests/repository-cti.test.js +195 -247
- package/orm/tests/repository-expiration.test.d.ts +1 -0
- package/orm/tests/repository-expiration.test.js +46 -59
- package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
- package/orm/tests/repository-extra-coverage.test.js +195 -337
- package/orm/tests/repository-mapping.test.d.ts +1 -0
- package/orm/tests/repository-mapping.test.js +20 -20
- package/orm/tests/repository-regression.test.js +124 -163
- package/orm/tests/repository-search.test.js +30 -44
- package/orm/tests/repository-soft-delete.test.js +54 -79
- package/orm/tests/repository-types.test.js +77 -111
- package/orm/tests/repository-undelete.test.d.ts +2 -0
- package/orm/tests/repository-undelete.test.js +201 -0
- package/package.json +3 -3
- package/task-queue/tests/worker.test.js +5 -5
- package/testing/README.md +38 -16
- package/testing/integration-setup.d.ts +11 -0
- package/testing/integration-setup.js +57 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.147",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
"type-fest": "^5.4"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
|
-
"@aws-sdk/client-s3": "^3.
|
|
156
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
155
|
+
"@aws-sdk/client-s3": "^3.1000",
|
|
156
|
+
"@aws-sdk/s3-request-presigner": "^3.1000",
|
|
157
157
|
"@genkit-ai/google-genai": "^1.29",
|
|
158
158
|
"@google-cloud/storage": "^7.19",
|
|
159
159
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import { CancellationToken } from '../../cancellation/index.js';
|
|
3
|
-
import {
|
|
4
|
-
import { injectRepository } from '../../orm/server/index.js';
|
|
3
|
+
import { getRepository } from '../../orm/server/index.js';
|
|
5
4
|
import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
6
5
|
import { PostgresTask } from '../../task-queue/postgres/index.js';
|
|
7
6
|
import { setupIntegrationTest } from '../../testing/index.js';
|
|
@@ -11,8 +10,10 @@ describe('Worker & Base Class Tests', () => {
|
|
|
11
10
|
let queue;
|
|
12
11
|
let token;
|
|
13
12
|
let otherQueueName;
|
|
13
|
+
let taskRepository;
|
|
14
14
|
beforeAll(async () => {
|
|
15
15
|
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
16
|
+
taskRepository = injector.resolve(getRepository(PostgresTask));
|
|
16
17
|
otherQueueName = `other-queue-${crypto.randomUUID()}`;
|
|
17
18
|
});
|
|
18
19
|
beforeEach(() => {
|
|
@@ -25,11 +26,10 @@ describe('Worker & Base Class Tests', () => {
|
|
|
25
26
|
});
|
|
26
27
|
afterEach(async () => {
|
|
27
28
|
token.set();
|
|
28
|
-
const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
|
|
29
29
|
const namespace = queue.getTransactionalContextData().namespace;
|
|
30
30
|
// Clear foreign keys
|
|
31
|
-
await
|
|
32
|
-
await
|
|
31
|
+
await taskRepository.updateManyByQuery({ namespace }, { parentId: null });
|
|
32
|
+
await taskRepository.updateManyByQuery({ namespace: otherQueueName }, { parentId: null });
|
|
33
33
|
await queue.clear();
|
|
34
34
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
35
35
|
await queueProvider.get(otherQueueName).clear();
|
package/testing/README.md
CHANGED
|
@@ -80,11 +80,16 @@ The `setupIntegrationTest` utility (found in `source/testing/integration-setup.t
|
|
|
80
80
|
4. **Schema Isolation**: Automatically creates and uses PostgreSQL schemas for isolation.
|
|
81
81
|
5. **Modules**: Optional configuration for `taskQueue`, `authentication`, `objectStorage`, `notification`, etc.
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Integration tests typically require an injection context to resolve services and repositories. You can use the `testInInjector` (or `itInInjector`) helper to automatically wrap your test body.
|
|
84
|
+
|
|
85
|
+
> [!IMPORTANT]
|
|
86
|
+
> `testInInjector` is designed for test files where a **single injector** is shared across all tests (typically initialized in `beforeAll`). It accepts a `ValueOrProvider<Injector>`, allowing you to pass a getter function (e.g., `() => injector`) if the injector variable is initialized late.
|
|
87
|
+
>
|
|
88
|
+
> If your tests require a fresh injector per test case (e.g. `setupIntegrationTest` in `beforeEach`), you can still use `testInInjector` by passing a provider.
|
|
84
89
|
|
|
85
90
|
```typescript
|
|
86
|
-
import { beforeAll, describe, expect
|
|
87
|
-
import { setupIntegrationTest } from '#/testing/index.js';
|
|
91
|
+
import { beforeAll, describe, expect } from 'vitest';
|
|
92
|
+
import { setupIntegrationTest, testInInjector } from '#/testing/index.js';
|
|
88
93
|
import { MyService } from '../my-service.js';
|
|
89
94
|
|
|
90
95
|
describe('MyService Integration', () => {
|
|
@@ -98,19 +103,32 @@ describe('MyService Integration', () => {
|
|
|
98
103
|
}));
|
|
99
104
|
});
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
testInInjector('should process data through the database', injector, async () => {
|
|
102
107
|
const service = injector.resolve(MyService);
|
|
103
108
|
const result = await service.process();
|
|
104
109
|
expect(result.success).toBe(true);
|
|
105
110
|
});
|
|
111
|
+
|
|
112
|
+
// Using a provider function to defer resolution of the 'injector' variable when using `beforeEach` for injector setup
|
|
113
|
+
testInInjector('should process data through the database', () => injector, async () => { ... });
|
|
106
114
|
});
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
## Best Practices & Guidelines
|
|
110
118
|
|
|
111
|
-
### 1. Isolation
|
|
112
|
-
|
|
113
|
-
-
|
|
119
|
+
### 1. Isolation & Performance
|
|
120
|
+
|
|
121
|
+
- **`beforeAll` vs `beforeEach`**:
|
|
122
|
+
- **Use `beforeAll` (Recommended)**: For most integration tests. It's significantly faster as it creates the schema and initializes the injector once.
|
|
123
|
+
- **Use `beforeEach`**: Use this only when:
|
|
124
|
+
- You need a completely clean Injector state for every test (e.g. if singletons are being mutated and cannot be reset).
|
|
125
|
+
- You are testing the initialization or disposal logic of your services.
|
|
126
|
+
- Each test requires a different module configuration in `setupIntegrationTest`.
|
|
127
|
+
- **Data Reset**:
|
|
128
|
+
- **`clearTenantData` (Best for Parallelism)**: If your tests use random `tenantId`s, use this in `afterEach` or `beforeEach`. It allows multiple tests within the same schema to run without interfering with each other's data.
|
|
129
|
+
- **`truncateTables` (Use with Caution)**: Only suitable if you are certain that no other tests are concurrently using the same database tables (even with different tenant IDs), as `TRUNCATE` affects the entire table and can cause flakiness in parallel environments.
|
|
130
|
+
- **Unit vs. Integration**: Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking.
|
|
131
|
+
- **Parallelism**: Vitest runs test files in parallel. Always use unique resource names (e.g., using `crypto.randomUUID()`) to avoid flaky tests and cross-file interference.
|
|
114
132
|
- **Resource Naming**: When testing shared infrastructure (Task Queues, S3 Buckets, Rate Limiters), append a random suffix or timestamp to the resource name (e.g., `const queueName = \`test-queue-${crypto.randomUUID()}\``).
|
|
115
133
|
- **Cleanup**: Use `truncateTables` or `dropTables` in `beforeAll` or `beforeEach` to ensure a clean state.
|
|
116
134
|
- **Tenant Data**: Use `clearTenantData` if your tests are multi-tenant and use random tenant IDs to avoid collisions when concurrently running tests.
|
|
@@ -124,24 +142,26 @@ describe('MyService Integration', () => {
|
|
|
124
142
|
const mailServiceMock = { send: vi.fn() };
|
|
125
143
|
injector.register(MailService, { useValue: mailServiceMock });
|
|
126
144
|
```
|
|
127
|
-
- **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in
|
|
145
|
+
- **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in an injection context.
|
|
146
|
+
- **Preferred**: Use `testInInjector` (or `itInInjector`) for the entire test body.
|
|
147
|
+
- **Alternative**: Use `runInInjectionContext(injector, async () => { ... })` for specific blocks within a test or hook.
|
|
128
148
|
- **Mock Reset**: Always call `vi.clearAllMocks()` in `beforeEach` if using spys or mocks to ensure test independence.
|
|
129
149
|
|
|
130
150
|
### 3. Integration Test Patterns
|
|
131
151
|
|
|
132
152
|
- **Schema Management**:
|
|
133
153
|
- **Preferred**: Use the `modules` option in `setupIntegrationTest`. This automatically runs the necessary migrations for standard modules (e.g., `taskQueue`, `authentication`, `notification`).
|
|
134
|
-
- **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)
|
|
154
|
+
- **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)` **only** for test-specific entities (like local `@Table` classes defined inside your test file) that are not part of a standard module. Always call `dropTables` before creating them to ensure a fresh start.
|
|
155
|
+
- **Repositories**: You can inject ad-hoc repositories inside an injection context:
|
|
156
|
+
```typescript
|
|
157
|
+
const repo = injectRepository(MyEntity);
|
|
158
|
+
```
|
|
135
159
|
- **Background Workers**: When testing workers or loops, use a `CancellationToken` to stop them in `afterEach` or `afterAll`.
|
|
160
|
+
- **Polling & Waiting**: When testing asynchronous background effects (like a task queue processing a task), avoid arbitrary `timeout()` calls if possible. Instead, use a polling loop or wait for a specific database state.
|
|
136
161
|
- **Timeouts**: Use the `timeout(ms)` helper from `#/utils/timing.js` for tests involving refills, retries, or background processing. Prefer `vi.useFakeTimers()` only when real-time passage is too slow for the test suite.
|
|
137
162
|
- **Logging**: Test should generally not output logs unless for active debugging. Use try-catch for expected errors and assert on the error message instead of relying on logs.
|
|
138
163
|
|
|
139
|
-
### 4.
|
|
140
|
-
|
|
141
|
-
- Use `beforeAll` for heavy setup (like schema and table creation) and `beforeEach` for lightweight data resetting (truncation).
|
|
142
|
-
- Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking and stubbing.
|
|
143
|
-
|
|
144
|
-
### 5. Coding Style
|
|
164
|
+
### 4. Coding Style
|
|
145
165
|
|
|
146
166
|
- **Explicit Types**: Use explicit return types for methods and functions.
|
|
147
167
|
- **Async/Await**: Always use `async/await` for asynchronous code to improve readability and error handling.
|
|
@@ -149,9 +169,11 @@ describe('MyService Integration', () => {
|
|
|
149
169
|
- **Avoid `any`**: Strive to use specific types or `unknown` if required to maintain type safety.
|
|
150
170
|
- **Descriptive Names**: Use descriptive names for test files, cases and variables to improve readability.
|
|
151
171
|
|
|
152
|
-
###
|
|
172
|
+
### 5. Common Helpers (from `source/testing/`)
|
|
153
173
|
|
|
154
174
|
- `setupIntegrationTest(options)`: Main entry point for integration tests.
|
|
175
|
+
- `testInInjector(name, injector, fn, options)`: Runs a test within an injection context.
|
|
176
|
+
- `itInInjector`: Alias for `testInInjector`.
|
|
155
177
|
- `truncateTables(database, schema, tables)`: Clears data from specified tables.
|
|
156
178
|
- `dropTables(database, schema, tables)`: Drops specified tables.
|
|
157
179
|
- `clearTenantData(database, schema, tables, tenantId)`: Deletes data for a specific tenant.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/useExpect: helper file */
|
|
1
2
|
import type { PoolConfig } from 'pg';
|
|
3
|
+
import { type TestOptions } from 'vitest';
|
|
2
4
|
import { type AuthenticationAncillaryService } from '../authentication/server/index.js';
|
|
3
5
|
import { Injector } from '../injector/index.js';
|
|
4
6
|
import { LogLevel } from '../logger/index.js';
|
|
@@ -6,6 +8,7 @@ import { type S3ObjectStorageProviderConfig } from '../object-storage/s3/index.j
|
|
|
6
8
|
import type { EntityType } from '../orm/entity.js';
|
|
7
9
|
import { Database } from '../orm/server/index.js';
|
|
8
10
|
import type { Type } from '../types/index.js';
|
|
11
|
+
import { type ValueOrProvider } from '../utils/value-or-provider.js';
|
|
9
12
|
export type IntegrationTestOptions = {
|
|
10
13
|
dbConfig?: Partial<PoolConfig>;
|
|
11
14
|
orm?: {
|
|
@@ -43,6 +46,14 @@ export type TestContext = {
|
|
|
43
46
|
* Standard setup for integration tests.
|
|
44
47
|
*/
|
|
45
48
|
export declare function setupIntegrationTest(options?: IntegrationTestOptions): Promise<TestContext>;
|
|
49
|
+
/**
|
|
50
|
+
* A wrapper for vitest's `test` that automatically runs the test function in the provided injector's context.
|
|
51
|
+
* @param name The name of the test.
|
|
52
|
+
* @param injector The injector to use for the context.
|
|
53
|
+
* @param fn The test function.
|
|
54
|
+
* @param options Vitest test options.
|
|
55
|
+
*/
|
|
56
|
+
export declare function testInInjector(name: string, injector: ValueOrProvider<Injector>, fn: () => any, options?: number | TestOptions): void;
|
|
46
57
|
/**
|
|
47
58
|
* Helper to truncate specific tables in a schema.
|
|
48
59
|
* Useful in beforeEach() to reset state.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/useExpect: helper file */
|
|
1
2
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { afterAll } from 'vitest';
|
|
3
|
+
import { afterAll, test } from 'vitest';
|
|
3
4
|
import { configureApiServer } from '../api/server/index.js';
|
|
4
5
|
import { configureAudit } from '../audit/index.js';
|
|
5
6
|
import { AuthenticationApiClient } from '../authentication/client/api.client.js';
|
|
@@ -26,7 +27,8 @@ import { configureDefaultSignalsImplementation } from '../signals/implementation
|
|
|
26
27
|
import { configurePostgresTaskQueue } from '../task-queue/postgres/index.js';
|
|
27
28
|
import * as configParser from '../utils/config-parser.js';
|
|
28
29
|
import { objectEntries } from '../utils/object/object.js';
|
|
29
|
-
import { assertDefinedPass, isDefined, isNotNull, isString } from '../utils/type-guards.js';
|
|
30
|
+
import { assertDefinedPass, isDefined, isNotNull, isNumber, isString, isUndefined } from '../utils/type-guards.js';
|
|
31
|
+
import { resolveValueOrProvider } from '../utils/value-or-provider.js';
|
|
30
32
|
/**
|
|
31
33
|
* Standard setup for integration tests.
|
|
32
34
|
*/
|
|
@@ -167,16 +169,22 @@ export async function setupIntegrationTest(options = {}) {
|
|
|
167
169
|
return { injector, database };
|
|
168
170
|
}
|
|
169
171
|
/**
|
|
170
|
-
*
|
|
172
|
+
* A wrapper for vitest's `test` that automatically runs the test function in the provided injector's context.
|
|
173
|
+
* @param name The name of the test.
|
|
174
|
+
* @param injector The injector to use for the context.
|
|
175
|
+
* @param fn The test function.
|
|
176
|
+
* @param options Vitest test options.
|
|
171
177
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
export function testInInjector(name, injector, fn, options) {
|
|
179
|
+
if (isUndefined(options) || isNumber(options)) {
|
|
180
|
+
test(name, async () => {
|
|
181
|
+
await runInInjectionContext(resolveValueOrProvider(injector), fn);
|
|
182
|
+
}, options);
|
|
177
183
|
}
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
else {
|
|
185
|
+
test(name, options, async () => {
|
|
186
|
+
await runInInjectionContext(resolveValueOrProvider(injector), fn);
|
|
187
|
+
});
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
/**
|
|
@@ -187,19 +195,15 @@ export async function truncateTables(database, schema, tables) {
|
|
|
187
195
|
if (tables.length == 0) {
|
|
188
196
|
return;
|
|
189
197
|
}
|
|
190
|
-
const
|
|
191
|
-
await database
|
|
192
|
-
try {
|
|
198
|
+
const lockName = `tstdl:maintenance:${schema}`;
|
|
199
|
+
await runWithAdvisoryLock(database, lockName, async () => {
|
|
193
200
|
// Using CASCADE to handle foreign keys automatically
|
|
194
201
|
for (const table of tables) {
|
|
195
202
|
const tableName = isString(table) ? table : getEntityTableName(table);
|
|
196
203
|
const tableSchema = isString(table) ? schema : getEntitySchema(table, schema);
|
|
197
204
|
await database.execute(sql `TRUNCATE TABLE ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
|
|
198
205
|
}
|
|
199
|
-
}
|
|
200
|
-
finally {
|
|
201
|
-
await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
|
|
202
|
-
}
|
|
206
|
+
});
|
|
203
207
|
}
|
|
204
208
|
/**
|
|
205
209
|
* Helper to delete data for a specific tenant from specific tables.
|
|
@@ -209,18 +213,15 @@ export async function clearTenantData(database, schema, tables, tenantId) {
|
|
|
209
213
|
if (tables.length == 0) {
|
|
210
214
|
return;
|
|
211
215
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
// Use a lock that is specific to both the schema AND the tenant
|
|
217
|
+
const lockName = `tstdl:maintenance:${schema ?? 'default'}:${tenantId}`;
|
|
218
|
+
await runWithAdvisoryLock(database, lockName, async () => {
|
|
215
219
|
for (const table of tables) {
|
|
216
220
|
const tableName = isString(table) ? table : getEntityTableName(table);
|
|
217
221
|
const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
|
|
218
222
|
await database.execute(sql `DELETE FROM ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} WHERE tenant_id = ${tenantId}`);
|
|
219
223
|
}
|
|
220
|
-
}
|
|
221
|
-
finally {
|
|
222
|
-
await database.execute(sql `SELECT pg_advisory_unlock(${lockId})`);
|
|
223
|
-
}
|
|
224
|
+
});
|
|
224
225
|
}
|
|
225
226
|
/**
|
|
226
227
|
* Helper to drop specific tables.
|
|
@@ -230,16 +231,42 @@ export async function dropTables(database, schema, tables) {
|
|
|
230
231
|
if (tables.length == 0) {
|
|
231
232
|
return;
|
|
232
233
|
}
|
|
233
|
-
const
|
|
234
|
-
await database
|
|
235
|
-
try {
|
|
234
|
+
const lockName = `tstdl:maintenance:${schema ?? 'default'}`;
|
|
235
|
+
await runWithAdvisoryLock(database, lockName, async () => {
|
|
236
236
|
for (const table of tables) {
|
|
237
237
|
const tableName = isString(table) ? table : getEntityTableName(table);
|
|
238
238
|
const tableSchema = isString(table) ? assertDefinedPass(schema, 'Schema not provided') : getEntitySchema(table, schema);
|
|
239
239
|
await database.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(tableSchema)}.${sql.identifier(tableName)} CASCADE`);
|
|
240
240
|
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Helper to run a migration safely with a database advisory lock to prevent race conditions.
|
|
245
|
+
*/
|
|
246
|
+
async function runMigrationSafely(database, migration) {
|
|
247
|
+
const lockName = 'tstdl:migration';
|
|
248
|
+
await runWithAdvisoryLock(database, lockName, migration);
|
|
249
|
+
}
|
|
250
|
+
async function runWithAdvisoryLock(database, lockName, fn) {
|
|
251
|
+
// We use pg_try_advisory_lock in a loop with small random delays to avoid deadlocks
|
|
252
|
+
// which sometimes happen with pg_advisory_lock under very high parallelism in Vitest
|
|
253
|
+
const start = Date.now();
|
|
254
|
+
const timeout = 30000; // 30s timeout for maintenance tasks
|
|
255
|
+
while (true) {
|
|
256
|
+
const { rows: [result] } = await database.execute(sql `SELECT pg_try_advisory_lock(hashtext(${lockName})) as locked`);
|
|
257
|
+
if (result?.locked) {
|
|
258
|
+
try {
|
|
259
|
+
await fn();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
await database.execute(sql `SELECT pg_advisory_unlock(hashtext(${lockName}))`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (Date.now() - start > timeout) {
|
|
267
|
+
throw new Error(`Timeout while waiting for advisory lock: ${lockName}`);
|
|
268
|
+
}
|
|
269
|
+
// Small random delay between 10ms and 100ms
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, 10 + Math.random() * 90));
|
|
244
271
|
}
|
|
245
272
|
}
|