@proteinjs/db 1.41.0 → 1.42.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/CHANGELOG.md +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/DetachedDbOps.d.ts +27 -0
- package/dist/src/DetachedDbOps.d.ts.map +1 -0
- package/dist/src/DetachedDbOps.js +60 -0
- package/dist/src/DetachedDbOps.js.map +1 -0
- package/dist/test/DetachedDbOps.test.d.ts +2 -0
- package/dist/test/DetachedDbOps.test.d.ts.map +1 -0
- package/dist/test/DetachedDbOps.test.js +158 -0
- package/dist/test/DetachedDbOps.test.js.map +1 -0
- package/index.ts +1 -0
- package/package.json +2 -2
- package/src/DetachedDbOps.ts +47 -0
- package/test/DetachedDbOps.test.ts +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.42.0](https://github.com/proteinjs/db/compare/@proteinjs/db@1.41.0...@proteinjs/db@1.42.0) (2026-09-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **db:** DetachedDbOps — THE named detachment idiom for deliberately fire-and-forget db work (one owner). Under node's default unhandled-rejection policy (node 24 in production) a detached db operation that rejects — e.g. a write tripping the driver's 60s op deadline — killed the whole process (observed live: a detached write past the deadline aborted the server). DetachedDbOps.run(description, work, context?) invokes the work, terminally catches (synchronous throws included), and logs the failure WITH the caller's context — never process death, never a silent swallow; returns void by design so a detached outcome cannot be accidentally consumed. Exported from the package root for every consumer's deliberate detached DML. The repo's own deliberate detached call sites route through it: SpannerLivenessMonitor's two detached verifyLiveness dispatches (pool-error listener + driver-reported error); the client-recycle oldDb.close stays on its existing terminal catch (its silent swallow is a named decision — the channel is believed dead). No half-existing db-layer idiom found to extend: the service layer's doNotAwait observer (ServiceExecutor) covers only service-dispatched work. RED RUN stated: DetachedDbOps.test red at the absent module (the simulated 60s-deadline driver rejection is the suite's first pin); green wired 4/4. Bites verified: terminal catch dropped -> the 2 rejection pins red; sync containment dropped -> the sync-throw pin red alone; restored green. Suites: packages/db 16 suites / 134 tests green (130 pre-existing + 4 new); drivers/spanner Liveness 1/1 green on a dedicated emulator (:9040) — the routed pool-error path exercised live; driver tsc clean (the one build red was the cloned stale db-query dist, cleared by rebuilding query from main source). ([3b92847](https://github.com/proteinjs/db/commit/3b92847355b23db7013fb9bf11fd1b6e7e879b72))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [1.41.0](https://github.com/proteinjs/db/compare/@proteinjs/db@1.40.0...@proteinjs/db@1.41.0) (2026-09-01)
|
|
7
18
|
|
|
8
19
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC;AACnD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAI9C,OAAO,EAAE,kBAAkB,IAAI,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAC/F,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,6CAA6C,CAAC;AAE5D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAE1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,wCAAwC,CAAC;AACvD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,iDAAiD,CAAC;AAEhE,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAE5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,uCAAuC,CAAC;AACtD,cAAc,yCAAyC,CAAC;AAExD,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC;AACnD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAI9C,OAAO,EAAE,kBAAkB,IAAI,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAC/F,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,6CAA6C,CAAC;AAE5D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAE1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,wCAAwC,CAAC;AACvD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,iDAAiD,CAAC;AAEhE,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAE5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,uCAAuC,CAAC;AACtD,cAAc,yCAAyC,CAAC;AAExD,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.SourceRecordSyncRunner = void 0;
|
|
18
18
|
__exportStar(require("./src/Db"), exports);
|
|
19
|
+
__exportStar(require("./src/DetachedDbOps"), exports);
|
|
19
20
|
__exportStar(require("./src/Table"), exports);
|
|
20
21
|
__exportStar(require("./src/RecordAccessError"), exports);
|
|
21
22
|
__exportStar(require("./src/auth/TableAuth"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,8CAA4B;AAC5B,0DAAwC;AACxC,uDAAqC;AACrC,8DAA4C;AAC5C,gDAA8B;AAC9B,+CAA6B;AAC7B,iEAA+C;AAC/C,qEAAmD;AACnD,4DAA0C;AAC1C,yDAAuC;AACvC,iEAA+C;AAC/C,uDAAqC;AACrC,0DAAwC;AACxC,4DAA0C;AAC1C,gEAA8C;AAC9C,+FAA+F;AAC/F,+FAA+F;AAC/F,gFAAgF;AAChF,sEAA+F;AAAtF,4HAAA,kBAAkB,OAA0B;AACrD,wDAAsC;AACtC,4DAA0C;AAC1C,qDAAmC;AACnC,gEAA8C;AAC9C,sEAAoD;AACpD,8EAA4D;AAE5D,gEAA8C;AAC9C,8DAA4C;AAC5C,4DAA0C;AAE1C,qEAAmD;AACnD,6EAA2D;AAC3D,sEAAoD;AACpD,0EAAwD;AACxD,gEAA8C;AAC9C,sEAAoD;AACpD,mEAAiD;AACjD,oEAAkD;AAClD,6EAA2D;AAC3D,iFAA+D;AAC/D,oEAAkD;AAClD,yEAAuD;AACvD,8EAA4D;AAC5D,6EAA2D;AAC3D,kFAAgE;AAEhE,sDAAoC;AACpC,8DAA4C;AAE5C,2DAAyC;AACzC,wEAAsD;AACtD,0EAAwD;AAExD,sDAAoC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,sDAAoC;AACpC,8CAA4B;AAC5B,0DAAwC;AACxC,uDAAqC;AACrC,8DAA4C;AAC5C,gDAA8B;AAC9B,+CAA6B;AAC7B,iEAA+C;AAC/C,qEAAmD;AACnD,4DAA0C;AAC1C,yDAAuC;AACvC,iEAA+C;AAC/C,uDAAqC;AACrC,0DAAwC;AACxC,4DAA0C;AAC1C,gEAA8C;AAC9C,+FAA+F;AAC/F,+FAA+F;AAC/F,gFAAgF;AAChF,sEAA+F;AAAtF,4HAAA,kBAAkB,OAA0B;AACrD,wDAAsC;AACtC,4DAA0C;AAC1C,qDAAmC;AACnC,gEAA8C;AAC9C,sEAAoD;AACpD,8EAA4D;AAE5D,gEAA8C;AAC9C,8DAA4C;AAC5C,4DAA0C;AAE1C,qEAAmD;AACnD,6EAA2D;AAC3D,sEAAoD;AACpD,0EAAwD;AACxD,gEAA8C;AAC9C,sEAAoD;AACpD,mEAAiD;AACjD,oEAAkD;AAClD,6EAA2D;AAC3D,iFAA+D;AAC/D,oEAAkD;AAClD,yEAAuD;AACvD,8EAA4D;AAC5D,6EAA2D;AAC3D,kFAAgE;AAEhE,sDAAoC;AACpC,8DAA4C;AAE5C,2DAAyC;AACzC,wEAAsD;AACtD,0EAAwD;AAExD,sDAAoC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE detachment idiom for deliberately fire-and-forget db work — one owner.
|
|
3
|
+
*
|
|
4
|
+
* Under node's default unhandled-rejection policy (node 15+ `--unhandled-rejections=throw`;
|
|
5
|
+
* node 24 in production), a detached db operation that REJECTS — e.g. a write that trips the
|
|
6
|
+
* driver's op deadline (`SpannerConfig.opDeadlineMs`, default 60s) — is an unhandled rejection
|
|
7
|
+
* that KILLS THE PROCESS (observed live: a detached write past the 60s driver deadline aborted
|
|
8
|
+
* the server). Detaching is a legitimate, named decision — the caller does not need the outcome
|
|
9
|
+
* to proceed — but the rejection still needs a terminal observer. This class is that observer:
|
|
10
|
+
* every deliberately un-awaited db operation routes through `run`, which invokes the work,
|
|
11
|
+
* terminally catches (synchronous throws included), and logs the failure WITH the caller's
|
|
12
|
+
* context — never process death, never a silent swallow.
|
|
13
|
+
*
|
|
14
|
+
* NOT for outcomes the caller depends on: work whose failure must alter behavior is awaited at
|
|
15
|
+
* the call site. `run` returns void by design, so a detached result cannot be accidentally
|
|
16
|
+
* consumed.
|
|
17
|
+
*/
|
|
18
|
+
export declare class DetachedDbOps {
|
|
19
|
+
private static logger;
|
|
20
|
+
/**
|
|
21
|
+
* Run `work` detached. Failures — a rejected promise or a synchronous throw — are terminally
|
|
22
|
+
* logged under `description` with `context`, never rethrown and never left unhandled.
|
|
23
|
+
*/
|
|
24
|
+
static run(description: string, work: () => Promise<unknown>, context?: Record<string, unknown>): void;
|
|
25
|
+
private static observe;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=DetachedDbOps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetachedDbOps.d.ts","sourceRoot":"","sources":["../../src/DetachedDbOps.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAyC;IAE9D;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAatG,OAAO,CAAC,MAAM,CAAC,OAAO;CAOvB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.DetachedDbOps = void 0;
|
|
15
|
+
var logger_1 = require("@proteinjs/logger");
|
|
16
|
+
/**
|
|
17
|
+
* THE detachment idiom for deliberately fire-and-forget db work — one owner.
|
|
18
|
+
*
|
|
19
|
+
* Under node's default unhandled-rejection policy (node 15+ `--unhandled-rejections=throw`;
|
|
20
|
+
* node 24 in production), a detached db operation that REJECTS — e.g. a write that trips the
|
|
21
|
+
* driver's op deadline (`SpannerConfig.opDeadlineMs`, default 60s) — is an unhandled rejection
|
|
22
|
+
* that KILLS THE PROCESS (observed live: a detached write past the 60s driver deadline aborted
|
|
23
|
+
* the server). Detaching is a legitimate, named decision — the caller does not need the outcome
|
|
24
|
+
* to proceed — but the rejection still needs a terminal observer. This class is that observer:
|
|
25
|
+
* every deliberately un-awaited db operation routes through `run`, which invokes the work,
|
|
26
|
+
* terminally catches (synchronous throws included), and logs the failure WITH the caller's
|
|
27
|
+
* context — never process death, never a silent swallow.
|
|
28
|
+
*
|
|
29
|
+
* NOT for outcomes the caller depends on: work whose failure must alter behavior is awaited at
|
|
30
|
+
* the call site. `run` returns void by design, so a detached result cannot be accidentally
|
|
31
|
+
* consumed.
|
|
32
|
+
*/
|
|
33
|
+
var DetachedDbOps = /** @class */ (function () {
|
|
34
|
+
function DetachedDbOps() {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run `work` detached. Failures — a rejected promise or a synchronous throw — are terminally
|
|
38
|
+
* logged under `description` with `context`, never rethrown and never left unhandled.
|
|
39
|
+
*/
|
|
40
|
+
DetachedDbOps.run = function (description, work, context) {
|
|
41
|
+
var _this = this;
|
|
42
|
+
var result;
|
|
43
|
+
try {
|
|
44
|
+
result = work();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.observe(description, error, context);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
result.catch(function (error) { return _this.observe(description, error, context); });
|
|
51
|
+
};
|
|
52
|
+
// --- helpers last ---
|
|
53
|
+
DetachedDbOps.observe = function (description, error, context) {
|
|
54
|
+
this.logger.error(__assign({ message: "Detached db operation failed: ".concat(description), error: error }, (context ? { obj: context } : {})));
|
|
55
|
+
};
|
|
56
|
+
DetachedDbOps.logger = new logger_1.Logger({ name: 'DetachedDbOps' });
|
|
57
|
+
return DetachedDbOps;
|
|
58
|
+
}());
|
|
59
|
+
exports.DetachedDbOps = DetachedDbOps;
|
|
60
|
+
//# sourceMappingURL=DetachedDbOps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetachedDbOps.js","sourceRoot":"","sources":["../../src/DetachedDbOps.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,4CAA2C;AAE3C;;;;;;;;;;;;;;;;GAgBG;AACH;IAAA;IA2BA,CAAC;IAxBC;;;OAGG;IACI,iBAAG,GAAV,UAAW,WAAmB,EAAE,IAA4B,EAAE,OAAiC;QAA/F,iBASC;QARC,IAAI,MAAwB,CAAC;QAC7B,IAAI;YACF,MAAM,GAAG,IAAI,EAAE,CAAC;SACjB;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAC1C,OAAO;SACR;QACD,MAAM,CAAC,KAAK,CAAC,UAAC,KAAK,IAAK,OAAA,KAAI,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,EAAzC,CAAyC,CAAC,CAAC;IACrE,CAAC;IAED,uBAAuB;IAER,qBAAO,GAAtB,UAAuB,WAAmB,EAAE,KAAc,EAAE,OAAiC;QAC3F,IAAI,CAAC,MAAM,CAAC,KAAK,YACf,OAAO,EAAE,wCAAiC,WAAW,CAAE,EACvD,KAAK,OAAA,IACF,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EACpC,CAAC;IACL,CAAC;IAzBc,oBAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;IA0BhE,oBAAC;CAAA,AA3BD,IA2BC;AA3BY,sCAAa"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetachedDbOps.test.d.ts","sourceRoot":"","sources":["../../test/DetachedDbOps.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
var logger_1 = require("@proteinjs/logger");
|
|
40
|
+
var DetachedDbOps_1 = require("../src/DetachedDbOps");
|
|
41
|
+
/**
|
|
42
|
+
* The detachment idiom's contract (the node-24 crash class): a deliberately fire-and-forget db
|
|
43
|
+
* operation that REJECTS — e.g. a write that trips the driver's 60s op deadline — must be
|
|
44
|
+
* terminally observed (logged with the caller's context), never left as an unhandled rejection
|
|
45
|
+
* for node's default policy to kill the process over. These tests run with no emulator: the
|
|
46
|
+
* simulated driver rejection IS the deadline failure's shape.
|
|
47
|
+
*
|
|
48
|
+
* Note: jest itself surfaces unhandled rejections as failures — so each green run here doubles
|
|
49
|
+
* as the no-escape assertion (a dropped catch turns the log pins red AND leaks the rejection).
|
|
50
|
+
*/
|
|
51
|
+
var flushDetached = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
52
|
+
return __generator(this, function (_a) {
|
|
53
|
+
switch (_a.label) {
|
|
54
|
+
case 0:
|
|
55
|
+
// The terminal catch attaches synchronously; the rejection lands on the microtask queue.
|
|
56
|
+
return [4 /*yield*/, Promise.resolve()];
|
|
57
|
+
case 1:
|
|
58
|
+
// The terminal catch attaches synchronously; the rejection lands on the microtask queue.
|
|
59
|
+
_a.sent();
|
|
60
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setImmediate(resolve); })];
|
|
61
|
+
case 2:
|
|
62
|
+
_a.sent();
|
|
63
|
+
return [2 /*return*/];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}); };
|
|
67
|
+
describe('DetachedDbOps', function () {
|
|
68
|
+
var errorLogs;
|
|
69
|
+
var errorSpy;
|
|
70
|
+
beforeEach(function () {
|
|
71
|
+
errorLogs = [];
|
|
72
|
+
errorSpy = jest.spyOn(logger_1.Logger.prototype, 'error').mockImplementation(function (log) {
|
|
73
|
+
errorLogs.push(log);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
afterEach(function () {
|
|
77
|
+
errorSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
test('a rejecting detached operation is terminally observed — logged with the caller context, no escaping rejection', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
80
|
+
var deadlineRejection;
|
|
81
|
+
return __generator(this, function (_a) {
|
|
82
|
+
switch (_a.label) {
|
|
83
|
+
case 0:
|
|
84
|
+
deadlineRejection = new Error('4 DEADLINE_EXCEEDED: spanner dml transaction (runTransactionAsync) exceeded the 60000ms op deadline');
|
|
85
|
+
DetachedDbOps_1.DetachedDbOps.run('write flow-run heartbeat', function () { return Promise.reject(deadlineRejection); }, {
|
|
86
|
+
flowRunId: 'run-1',
|
|
87
|
+
table: 'flow_run',
|
|
88
|
+
});
|
|
89
|
+
return [4 /*yield*/, flushDetached()];
|
|
90
|
+
case 1:
|
|
91
|
+
_a.sent();
|
|
92
|
+
expect(errorLogs).toHaveLength(1);
|
|
93
|
+
expect(errorLogs[0].message).toContain('Detached db operation failed');
|
|
94
|
+
expect(errorLogs[0].message).toContain('write flow-run heartbeat');
|
|
95
|
+
expect(errorLogs[0].obj).toEqual({ flowRunId: 'run-1', table: 'flow_run' });
|
|
96
|
+
expect(errorLogs[0].error).toBe(deadlineRejection);
|
|
97
|
+
return [2 /*return*/];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}); });
|
|
101
|
+
test('a synchronously-throwing work function is contained the same way — the caller never sees the throw', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
102
|
+
var boom;
|
|
103
|
+
return __generator(this, function (_a) {
|
|
104
|
+
switch (_a.label) {
|
|
105
|
+
case 0:
|
|
106
|
+
boom = new Error('driver construction failed before any promise existed');
|
|
107
|
+
expect(function () {
|
|
108
|
+
return DetachedDbOps_1.DetachedDbOps.run('sync-throwing detached op', function () {
|
|
109
|
+
throw boom;
|
|
110
|
+
}, { site: 'test' });
|
|
111
|
+
}).not.toThrow();
|
|
112
|
+
return [4 /*yield*/, flushDetached()];
|
|
113
|
+
case 1:
|
|
114
|
+
_a.sent();
|
|
115
|
+
expect(errorLogs).toHaveLength(1);
|
|
116
|
+
expect(errorLogs[0].message).toContain('sync-throwing detached op');
|
|
117
|
+
expect(errorLogs[0].error).toBe(boom);
|
|
118
|
+
return [2 /*return*/];
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}); });
|
|
122
|
+
test('a resolving detached operation logs nothing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
123
|
+
var ran;
|
|
124
|
+
return __generator(this, function (_a) {
|
|
125
|
+
switch (_a.label) {
|
|
126
|
+
case 0:
|
|
127
|
+
ran = false;
|
|
128
|
+
DetachedDbOps_1.DetachedDbOps.run('healthy detached op', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
129
|
+
return __generator(this, function (_a) {
|
|
130
|
+
ran = true;
|
|
131
|
+
return [2 /*return*/, 'ok'];
|
|
132
|
+
});
|
|
133
|
+
}); });
|
|
134
|
+
return [4 /*yield*/, flushDetached()];
|
|
135
|
+
case 1:
|
|
136
|
+
_a.sent();
|
|
137
|
+
expect(ran).toBe(true);
|
|
138
|
+
expect(errorLogs).toHaveLength(0);
|
|
139
|
+
return [2 /*return*/];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}); });
|
|
143
|
+
test('context is optional — a rejection without context still logs the description', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
144
|
+
return __generator(this, function (_a) {
|
|
145
|
+
switch (_a.label) {
|
|
146
|
+
case 0:
|
|
147
|
+
DetachedDbOps_1.DetachedDbOps.run('context-free detached op', function () { return Promise.reject(new Error('deadline')); });
|
|
148
|
+
return [4 /*yield*/, flushDetached()];
|
|
149
|
+
case 1:
|
|
150
|
+
_a.sent();
|
|
151
|
+
expect(errorLogs).toHaveLength(1);
|
|
152
|
+
expect(errorLogs[0].message).toContain('context-free detached op');
|
|
153
|
+
return [2 /*return*/];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}); });
|
|
157
|
+
});
|
|
158
|
+
//# sourceMappingURL=DetachedDbOps.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DetachedDbOps.test.js","sourceRoot":"","sources":["../../test/DetachedDbOps.test.ts"],"names":[],"mappings":"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"}
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.0",
|
|
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": "bbbbd77251d6c48bd4dd31331c2bbb8113337597"
|
|
70
70
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Logger } from '@proteinjs/logger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE detachment idiom for deliberately fire-and-forget db work — one owner.
|
|
5
|
+
*
|
|
6
|
+
* Under node's default unhandled-rejection policy (node 15+ `--unhandled-rejections=throw`;
|
|
7
|
+
* node 24 in production), a detached db operation that REJECTS — e.g. a write that trips the
|
|
8
|
+
* driver's op deadline (`SpannerConfig.opDeadlineMs`, default 60s) — is an unhandled rejection
|
|
9
|
+
* that KILLS THE PROCESS (observed live: a detached write past the 60s driver deadline aborted
|
|
10
|
+
* the server). Detaching is a legitimate, named decision — the caller does not need the outcome
|
|
11
|
+
* to proceed — but the rejection still needs a terminal observer. This class is that observer:
|
|
12
|
+
* every deliberately un-awaited db operation routes through `run`, which invokes the work,
|
|
13
|
+
* terminally catches (synchronous throws included), and logs the failure WITH the caller's
|
|
14
|
+
* context — never process death, never a silent swallow.
|
|
15
|
+
*
|
|
16
|
+
* NOT for outcomes the caller depends on: work whose failure must alter behavior is awaited at
|
|
17
|
+
* the call site. `run` returns void by design, so a detached result cannot be accidentally
|
|
18
|
+
* consumed.
|
|
19
|
+
*/
|
|
20
|
+
export class DetachedDbOps {
|
|
21
|
+
private static logger = new Logger({ name: 'DetachedDbOps' });
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run `work` detached. Failures — a rejected promise or a synchronous throw — are terminally
|
|
25
|
+
* logged under `description` with `context`, never rethrown and never left unhandled.
|
|
26
|
+
*/
|
|
27
|
+
static run(description: string, work: () => Promise<unknown>, context?: Record<string, unknown>): void {
|
|
28
|
+
let result: Promise<unknown>;
|
|
29
|
+
try {
|
|
30
|
+
result = work();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
this.observe(description, error, context);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
result.catch((error) => this.observe(description, error, context));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- helpers last ---
|
|
39
|
+
|
|
40
|
+
private static observe(description: string, error: unknown, context?: Record<string, unknown>): void {
|
|
41
|
+
this.logger.error({
|
|
42
|
+
message: `Detached db operation failed: ${description}`,
|
|
43
|
+
error,
|
|
44
|
+
...(context ? { obj: context } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Logger } from '@proteinjs/logger';
|
|
2
|
+
import { DetachedDbOps } from '../src/DetachedDbOps';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The detachment idiom's contract (the node-24 crash class): a deliberately fire-and-forget db
|
|
6
|
+
* operation that REJECTS — e.g. a write that trips the driver's 60s op deadline — must be
|
|
7
|
+
* terminally observed (logged with the caller's context), never left as an unhandled rejection
|
|
8
|
+
* for node's default policy to kill the process over. These tests run with no emulator: the
|
|
9
|
+
* simulated driver rejection IS the deadline failure's shape.
|
|
10
|
+
*
|
|
11
|
+
* Note: jest itself surfaces unhandled rejections as failures — so each green run here doubles
|
|
12
|
+
* as the no-escape assertion (a dropped catch turns the log pins red AND leaks the rejection).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const flushDetached = async () => {
|
|
16
|
+
// The terminal catch attaches synchronously; the rejection lands on the microtask queue.
|
|
17
|
+
await Promise.resolve();
|
|
18
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('DetachedDbOps', () => {
|
|
22
|
+
let errorLogs: Array<{ message: string; obj?: Record<string, unknown>; error?: unknown }>;
|
|
23
|
+
let errorSpy: jest.SpyInstance;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
errorLogs = [];
|
|
27
|
+
errorSpy = jest.spyOn(Logger.prototype, 'error').mockImplementation(function (log: any) {
|
|
28
|
+
errorLogs.push(log);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
errorSpy.mockRestore();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('a rejecting detached operation is terminally observed — logged with the caller context, no escaping rejection', async () => {
|
|
37
|
+
const deadlineRejection = new Error(
|
|
38
|
+
'4 DEADLINE_EXCEEDED: spanner dml transaction (runTransactionAsync) exceeded the 60000ms op deadline'
|
|
39
|
+
);
|
|
40
|
+
DetachedDbOps.run('write flow-run heartbeat', () => Promise.reject(deadlineRejection), {
|
|
41
|
+
flowRunId: 'run-1',
|
|
42
|
+
table: 'flow_run',
|
|
43
|
+
});
|
|
44
|
+
await flushDetached();
|
|
45
|
+
|
|
46
|
+
expect(errorLogs).toHaveLength(1);
|
|
47
|
+
expect(errorLogs[0].message).toContain('Detached db operation failed');
|
|
48
|
+
expect(errorLogs[0].message).toContain('write flow-run heartbeat');
|
|
49
|
+
expect(errorLogs[0].obj).toEqual({ flowRunId: 'run-1', table: 'flow_run' });
|
|
50
|
+
expect(errorLogs[0].error).toBe(deadlineRejection);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('a synchronously-throwing work function is contained the same way — the caller never sees the throw', async () => {
|
|
54
|
+
const boom = new Error('driver construction failed before any promise existed');
|
|
55
|
+
expect(() =>
|
|
56
|
+
DetachedDbOps.run(
|
|
57
|
+
'sync-throwing detached op',
|
|
58
|
+
() => {
|
|
59
|
+
throw boom;
|
|
60
|
+
},
|
|
61
|
+
{ site: 'test' }
|
|
62
|
+
)
|
|
63
|
+
).not.toThrow();
|
|
64
|
+
await flushDetached();
|
|
65
|
+
|
|
66
|
+
expect(errorLogs).toHaveLength(1);
|
|
67
|
+
expect(errorLogs[0].message).toContain('sync-throwing detached op');
|
|
68
|
+
expect(errorLogs[0].error).toBe(boom);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a resolving detached operation logs nothing', async () => {
|
|
72
|
+
let ran = false;
|
|
73
|
+
DetachedDbOps.run('healthy detached op', async () => {
|
|
74
|
+
ran = true;
|
|
75
|
+
return 'ok';
|
|
76
|
+
});
|
|
77
|
+
await flushDetached();
|
|
78
|
+
|
|
79
|
+
expect(ran).toBe(true);
|
|
80
|
+
expect(errorLogs).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('context is optional — a rejection without context still logs the description', async () => {
|
|
84
|
+
DetachedDbOps.run('context-free detached op', () => Promise.reject(new Error('deadline')));
|
|
85
|
+
await flushDetached();
|
|
86
|
+
|
|
87
|
+
expect(errorLogs).toHaveLength(1);
|
|
88
|
+
expect(errorLogs[0].message).toContain('context-free detached op');
|
|
89
|
+
});
|
|
90
|
+
});
|