@ultimat3/testing 27.11.0 → 27.12.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/README.md CHANGED
@@ -305,9 +305,15 @@ its own, so one shared store is enough.
305
305
  ## Parallel databases
306
306
 
307
307
  ```ts
308
- const db = await acquireWorkerDatabase({ adminUrl, migrate });
308
+ import { acquireWorkerDatabase } from '@ultimat3/testing';
309
+
310
+ // `migrate: (url) => …` applies the app's schema into the template once; omitted here.
311
+ await using db = await acquireWorkerDatabase({ adminUrl: Bun.env['TEST_DATABASE_URL'] });
309
312
  ```
310
313
 
314
+ The handle is async-disposable (`As of 2026-10-18`): `await using` drops the worker's database when
315
+ the scope ends, a failed test included; `db.drop()` is the same call spelled by hand.
316
+
311
317
  The first worker creates the template under a Postgres advisory lock and migrates it; every
312
318
  worker then clones it copy-on-write, under the same lock — a clone waits only 5 s for a session on
313
319
  the template to leave, and the next worker's migration holds one. With no Postgres configured it falls back to PGlite, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "27.11.0",
3
+ "version": "27.12.0",
4
4
  "description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,20 +36,20 @@
36
36
  "test": "bun test"
37
37
  },
38
38
  "dependencies": {
39
- "@ultimat3/action": "27.11.0",
40
- "@ultimat3/cache": "27.11.0",
41
- "@ultimat3/core": "27.11.0",
42
- "@ultimat3/db": "27.11.0",
43
- "@ultimat3/entity": "27.11.0",
44
- "@ultimat3/http": "27.11.0",
45
- "@ultimat3/i18n": "27.11.0",
46
- "@ultimat3/jobs": "27.11.0",
47
- "@ultimat3/mail": "27.11.0",
48
- "@ultimat3/policy": "27.11.0",
49
- "@ultimat3/pwa": "27.11.0",
50
- "@ultimat3/query": "27.11.0",
51
- "@ultimat3/realtime": "27.11.0",
52
- "@ultimat3/render": "27.11.0",
53
- "@ultimat3/time": "27.11.0"
39
+ "@ultimat3/action": "27.12.0",
40
+ "@ultimat3/cache": "27.12.0",
41
+ "@ultimat3/core": "27.12.0",
42
+ "@ultimat3/db": "27.12.0",
43
+ "@ultimat3/entity": "27.12.0",
44
+ "@ultimat3/http": "27.12.0",
45
+ "@ultimat3/i18n": "27.12.0",
46
+ "@ultimat3/jobs": "27.12.0",
47
+ "@ultimat3/mail": "27.12.0",
48
+ "@ultimat3/policy": "27.12.0",
49
+ "@ultimat3/pwa": "27.12.0",
50
+ "@ultimat3/query": "27.12.0",
51
+ "@ultimat3/realtime": "27.12.0",
52
+ "@ultimat3/render": "27.12.0",
53
+ "@ultimat3/time": "27.12.0"
54
54
  }
55
55
  }
package/src/index.ts CHANGED
@@ -340,7 +340,12 @@ export {
340
340
  } from './sealed-network';
341
341
  export type { SharedExamples } from './shared-examples';
342
342
  export { behavesLike, sharedExamples } from './shared-examples';
343
- export type { SqlRunner, TemplateDbConfig, WorkerDatabase } from './template-db';
343
+ export type {
344
+ DisposableWorkerDatabase,
345
+ SqlRunner,
346
+ TemplateDbConfig,
347
+ WorkerDatabase,
348
+ } from './template-db';
344
349
  export {
345
350
  acquireWorkerDatabase,
346
351
  cloneSql,
@@ -30,6 +30,13 @@ export interface WorkerDatabase {
30
30
  drop(): Promise<void>;
31
31
  }
32
32
 
33
+ /**
34
+ * What `acquireWorkerDatabase` answers: the handle plus `drop()` as its async disposer, so
35
+ * `await using db = await acquireWorkerDatabase()` releases it however the scope exits. A separate
36
+ * type so a hand-built `WorkerDatabase` (a fake `acquire`) owes no disposer.
37
+ */
38
+ export type DisposableWorkerDatabase = WorkerDatabase & AsyncDisposable;
39
+
33
40
  export const DEFAULT_TEMPLATE = 'ultimate_test_template';
34
41
 
35
42
  /**
@@ -109,7 +116,7 @@ const defaultConnect = (url: string): SqlRunner => {
109
116
  export async function acquireWorkerDatabase(
110
117
  config: TemplateDbConfig = {},
111
118
  deps: Partial<TemplateDbDeps> = {},
112
- ): Promise<WorkerDatabase> {
119
+ ): Promise<DisposableWorkerDatabase> {
113
120
  const env = deps.env ?? (Bun.env as Readonly<Record<string, string | undefined>>);
114
121
  const connect = deps.connect ?? defaultConnect;
115
122
  const template = config.templateName ?? DEFAULT_TEMPLATE;
@@ -118,13 +125,13 @@ export async function acquireWorkerDatabase(
118
125
  const adminUrl = config.adminUrl ?? env['TEST_DATABASE_URL'] ?? env['DATABASE_URL'];
119
126
 
120
127
  if (adminUrl === undefined || adminUrl.length === 0) {
121
- return {
128
+ return disposable({
122
129
  kind: 'pglite',
123
130
  worker,
124
131
  database,
125
132
  url: `pglite://memory/${database}`,
126
133
  drop: async () => undefined,
127
- };
134
+ });
128
135
  }
129
136
 
130
137
  const admin = connect(adminUrl);
@@ -160,7 +167,7 @@ export async function acquireWorkerDatabase(
160
167
  }
161
168
  await admin.close();
162
169
 
163
- return {
170
+ return disposable({
164
171
  kind: 'postgres',
165
172
  worker,
166
173
  database,
@@ -170,7 +177,12 @@ export async function acquireWorkerDatabase(
170
177
  await cleanup.exec(dropSql(database));
171
178
  await cleanup.close();
172
179
  },
173
- };
180
+ });
181
+ }
182
+
183
+ /** The handle with its `drop` as the async disposer, so `await using` is the one release spelling. */
184
+ function disposable(db: WorkerDatabase): DisposableWorkerDatabase {
185
+ return { ...db, [Symbol.asyncDispose]: () => db.drop() };
174
186
  }
175
187
 
176
188
  /**