@proteinjs/db 1.34.0 → 1.34.2
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/CHANGELOG.md +22 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/Db.d.ts +0 -1
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +14 -3
- package/dist/src/Db.js.map +1 -1
- package/dist/src/MigrationRunner.d.ts +10 -0
- package/dist/src/MigrationRunner.d.ts.map +1 -1
- package/dist/src/MigrationRunner.js +21 -2
- package/dist/src/MigrationRunner.js.map +1 -1
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.d.ts +2 -0
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.d.ts.map +1 -0
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.js +62 -0
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +1 -1
- package/package.json +2 -2
- package/src/Db.ts +16 -4
- package/src/MigrationRunner.ts +21 -2
- package/test/DbDefaultDriverDuplicateModuleState.test.ts +70 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.2",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "ead198c6ceceff2a37853c6211895b444575295c"
|
|
70
70
|
}
|
package/src/Db.ts
CHANGED
|
@@ -41,6 +41,19 @@ export const getDbAsSystem = <R extends Record = Record>() => new Db<R>(undefine
|
|
|
41
41
|
const getEnvVar = (key: string): string | undefined =>
|
|
42
42
|
typeof process !== 'undefined' && process.env ? process.env[key] : undefined;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The lazily-resolved default driver lives on the global object (window in browser, globalThis
|
|
46
|
+
* elsewhere) rather than on a class static: per-package installs can put multiple live copies of
|
|
47
|
+
* @proteinjs/db in one process (each sibling package's nested node_modules hosts its own copy),
|
|
48
|
+
* and a per-copy static means every copy asks the DefaultDbDriverFactory for its own driver —
|
|
49
|
+
* multiple live db clients per process (two Spanner clients holding gRPC channels open was the
|
|
50
|
+
* 2026-08-14 thought-ui jest-never-exits incident). Anchoring on the global gives every copy the
|
|
51
|
+
* one driver — same pattern as reflection's SourceRepository and this package's ReferenceCache.
|
|
52
|
+
*/
|
|
53
|
+
const DEFAULT_DB_DRIVER_GLOBAL_KEY = '__proteinjs_db_defaultDbDriver';
|
|
54
|
+
|
|
55
|
+
const getGlobal = (): any => (typeof window !== 'undefined' ? window : globalThis);
|
|
56
|
+
|
|
44
57
|
export type DbDriverQueryStatementConfig = ParameterizationConfig & {
|
|
45
58
|
prefixTablesWithDb?: boolean;
|
|
46
59
|
getDriverColumnType?: (tableName: string, columnName: string) => string;
|
|
@@ -79,7 +92,6 @@ export interface DbDriver {
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
export class Db<R extends Record = Record> implements DbService<R> {
|
|
82
|
-
private static defaultDbDriver: DbDriver;
|
|
83
95
|
private dbDriver: DbDriver;
|
|
84
96
|
private getTable: (tableName: string) => Table<any>;
|
|
85
97
|
private logger = new Logger({ name: this.constructor.name, logLevel: getEnvVar('DB_LOG_LEVEL') as any });
|
|
@@ -108,7 +120,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
static getDefaultDbDriver(): DbDriver {
|
|
111
|
-
if (!
|
|
123
|
+
if (!getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY]) {
|
|
112
124
|
const defaultDbDriverFactory = SourceRepository.get().object<DefaultDbDriverFactory>(
|
|
113
125
|
'@proteinjs/db/DefaultDbDriverFactory'
|
|
114
126
|
);
|
|
@@ -118,10 +130,10 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
118
130
|
);
|
|
119
131
|
}
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY] = defaultDbDriverFactory.getDbDriver();
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
return
|
|
136
|
+
return getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY];
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
private getDefaultTransactionContextFactory(): DefaultTransactionContextFactory {
|
package/src/MigrationRunner.ts
CHANGED
|
@@ -206,7 +206,7 @@ export class MigrationRunner implements MigrationRunnerService {
|
|
|
206
206
|
const db = getRunDb();
|
|
207
207
|
migration.status = 'running';
|
|
208
208
|
migration.startTime = moment();
|
|
209
|
-
await db.update(migrationTable, migration);
|
|
209
|
+
await db.update(migrationTable, this.definedFields(migration));
|
|
210
210
|
this.logger.info({ message: `Running migration (${migration.id}) ${migration.description}` });
|
|
211
211
|
try {
|
|
212
212
|
migration.output = await migration.run();
|
|
@@ -222,12 +222,31 @@ export class MigrationRunner implements MigrationRunnerService {
|
|
|
222
222
|
migration.endTime = moment();
|
|
223
223
|
}
|
|
224
224
|
migration.duration = this.duration(migration.startTime, migration.endTime);
|
|
225
|
-
await db.update(migrationTable, migration);
|
|
225
|
+
await db.update(migrationTable, this.definedFields(migration));
|
|
226
226
|
this.logger.info({
|
|
227
227
|
message: `[${migration.status}] (${migration.duration}) Finished running migration (${migration.id}) ${migration.description}`,
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The run-state payload with `undefined`-valued fields OMITTED. Several of the record's fields
|
|
233
|
+
* are legitimately absent depending on the run (`output` for a void `run()`, `failureMessage`/
|
|
234
|
+
* `failureStack` for a non-Error throw), but the migration object carries them as explicit
|
|
235
|
+
* `undefined` assignments — and `RecordSerializer` rejects any payload field holding `undefined`
|
|
236
|
+
* (never a partial write), which would strand the row at 'running' status with the run's real
|
|
237
|
+
* outcome lost. Absent means omitted, never undefined — for EVERY optional field of the payload,
|
|
238
|
+
* not per-field.
|
|
239
|
+
*/
|
|
240
|
+
private definedFields(migration: Migration): Partial<Migration> {
|
|
241
|
+
const payload: Partial<Migration> = {};
|
|
242
|
+
for (const [field, value] of Object.entries(migration)) {
|
|
243
|
+
if (value !== undefined) {
|
|
244
|
+
(payload as any)[field] = value;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return payload;
|
|
248
|
+
}
|
|
249
|
+
|
|
231
250
|
private resolveMigration(migrationTable: Table<Migration>, id: string): Migration {
|
|
232
251
|
const migration = new SourceRecordRepo().getSourceRecord<Migration>(migrationTable.name, id);
|
|
233
252
|
if (!migration) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate-module-instance guard for the default db driver (task #36).
|
|
3
|
+
*
|
|
4
|
+
* Per-package installs can put two live copies of @proteinjs/db in one process (each sibling
|
|
5
|
+
* package's nested node_modules hosts its own registry copy). `Db.defaultDbDriver` as a class
|
|
6
|
+
* static then splits per copy: every copy that lazily initializes asks the (globally converged,
|
|
7
|
+
* reflection-routed) DefaultDbDriverFactory for a driver, and the factory mints a fresh one per
|
|
8
|
+
* call — two live Spanner clients holding two gRPC channel sets. That is the 2026-08-14
|
|
9
|
+
* thought-ui incident: jest finished green and then never exited, the second universe's Spanner
|
|
10
|
+
* client keeping the process alive.
|
|
11
|
+
*
|
|
12
|
+
* `jest.isolateModules` reproduces the split semantics exactly: each isolated registry gets its
|
|
13
|
+
* own module instance of Db with its own statics, the same way two nested install paths do. The
|
|
14
|
+
* guard asserts every live copy resolves the SAME driver instance and the factory is consulted
|
|
15
|
+
* once — which only holds when the resolved driver is anchored on the process global, not on a
|
|
16
|
+
* per-copy class static.
|
|
17
|
+
*/
|
|
18
|
+
import { SourceRepository } from '@proteinjs/reflection';
|
|
19
|
+
|
|
20
|
+
type DbModule = typeof import('../src/Db');
|
|
21
|
+
|
|
22
|
+
const loadIsolatedCopy = (): DbModule => {
|
|
23
|
+
let copy: DbModule | undefined;
|
|
24
|
+
jest.isolateModules(() => {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
26
|
+
copy = require('../src/Db');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!copy) {
|
|
30
|
+
throw new Error('Failed to load an isolated copy of Db');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return copy;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Db default driver under duplicate module instances', () => {
|
|
37
|
+
let driversMinted: number;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
driversMinted = 0;
|
|
41
|
+
// House test pattern: seed the (globalThis-anchored, therefore converged) SourceRepository
|
|
42
|
+
// object cache directly. The factory mints a fresh driver object per call, mirroring real
|
|
43
|
+
// DefaultDbDriverFactory implementations (e.g. the app's SpannerDriver factory).
|
|
44
|
+
(SourceRepository.get() as any).objectCache['@proteinjs/db/DefaultDbDriverFactory'] = [
|
|
45
|
+
{
|
|
46
|
+
getDbDriver: () => {
|
|
47
|
+
driversMinted++;
|
|
48
|
+
return { minted: driversMinted };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
delete (SourceRepository.get() as any).objectCache['@proteinjs/db/DefaultDbDriverFactory'];
|
|
56
|
+
delete (globalThis as any).__proteinjs_db_defaultDbDriver;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('every live copy of Db resolves the same default driver instance', () => {
|
|
60
|
+
const copyA = loadIsolatedCopy();
|
|
61
|
+
const copyB = loadIsolatedCopy();
|
|
62
|
+
expect(copyA).not.toBe(copyB); // two distinct module instances, as under per-package installs
|
|
63
|
+
|
|
64
|
+
const driverA = copyA.Db.getDefaultDbDriver();
|
|
65
|
+
const driverB = copyB.Db.getDefaultDbDriver();
|
|
66
|
+
|
|
67
|
+
expect(driverB).toBe(driverA); // one process, one driver — one live client
|
|
68
|
+
expect(driversMinted).toBe(1); // the factory is consulted exactly once per process
|
|
69
|
+
});
|
|
70
|
+
});
|