@uns-kit/api 3.0.38 → 3.0.39

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/dist/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Module dependencies.
3
3
  */
4
- import { type Router } from "express";
4
+ import { type Response, type Router } from "express";
5
5
  import * as http from "http";
6
6
  type MountConfig = {
7
7
  apiBasePrefix?: string;
@@ -12,8 +12,16 @@ export default class App {
12
12
  private expressApplication;
13
13
  private ownershipGate;
14
14
  private readonly acceptedRequests;
15
+ private readonly requestWork;
15
16
  private requestFailed;
16
17
  setOwnershipGate(gate: () => boolean): void;
18
+ /**
19
+ * Declare application-owned read-only work before its first asynchronous step.
20
+ * HTTP method names are not proof of safety: only use this for work without
21
+ * external effects. A disconnected reader still waits for actual work to settle.
22
+ * This does not authenticate, cancel, retry or send the response.
23
+ */
24
+ runReadOnlyRequest<T>(res: Response, operation: () => T | Promise<T>): Promise<T>;
17
25
  drainRequests(): Promise<void>;
18
26
  stop(): Promise<void>;
19
27
  server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
package/dist/app.js CHANGED
@@ -30,8 +30,36 @@ export default class App {
30
30
  expressApplication;
31
31
  ownershipGate;
32
32
  acceptedRequests = new Set();
33
+ requestWork = new WeakMap();
33
34
  requestFailed = false;
34
35
  setOwnershipGate(gate) { this.ownershipGate = gate; }
36
+ /**
37
+ * Declare application-owned read-only work before its first asynchronous step.
38
+ * HTTP method names are not proof of safety: only use this for work without
39
+ * external effects. A disconnected reader still waits for actual work to settle.
40
+ * This does not authenticate, cancel, retry or send the response.
41
+ */
42
+ async runReadOnlyRequest(res, operation) {
43
+ const request = this.requestWork.get(res);
44
+ if (!request && this.ownershipGate)
45
+ throw new Error("HTTP request was not admitted by this runtime");
46
+ if (request) {
47
+ if (request.transportEnded || request.readOnly)
48
+ throw new Error("HTTP work must be declared once before the response ends");
49
+ request.readOnly = true;
50
+ request.workSettled = false;
51
+ }
52
+ try {
53
+ return await operation();
54
+ }
55
+ finally {
56
+ if (request) {
57
+ request.workSettled = true;
58
+ if (request.transportEnded)
59
+ request.complete();
60
+ }
61
+ }
62
+ }
35
63
  async drainRequests() {
36
64
  while (this.acceptedRequests.size)
37
65
  await Promise.all([...this.acceptedRequests]);
@@ -71,13 +99,20 @@ export default class App {
71
99
  let finish;
72
100
  const accepted = new Promise(resolve => { finish = resolve; });
73
101
  this.acceptedRequests.add(accepted);
74
- res.once("finish", () => { this.acceptedRequests.delete(accepted); finish(); });
75
- res.once("close", () => {
76
- if (!res.writableFinished)
102
+ const request = {
103
+ transportEnded: false, readOnly: false, workSettled: true,
104
+ complete: () => { this.acceptedRequests.delete(accepted); finish(); },
105
+ };
106
+ this.requestWork.set(res, request);
107
+ const endTransport = () => {
108
+ request.transportEnded = true;
109
+ if (!res.writableFinished && !request.readOnly)
77
110
  this.requestFailed = true;
78
- this.acceptedRequests.delete(accepted);
79
- finish();
80
- });
111
+ if (request.workSettled)
112
+ request.complete();
113
+ };
114
+ res.once("finish", endTransport);
115
+ res.once("close", endTransport);
81
116
  next();
82
117
  });
83
118
  this.processName = processName;
@@ -1,3 +1,4 @@
1
+ import { type Response } from "express";
1
2
  import { UnsAttribute } from "@uns-kit/core/uns/uns-interfaces.js";
2
3
  import UnsProxy from "@uns-kit/core/uns/uns-proxy.js";
3
4
  import { UnsTopics } from "@uns-kit/core/uns/uns-topics.js";
@@ -23,6 +24,8 @@ export default class UnsApiProxy extends UnsProxy {
23
24
  constructor(processName: string, instanceName: string, options: IApiProxyOptions);
24
25
  setOwnershipGate(gate: () => boolean): void;
25
26
  drainRequests(): Promise<void>;
27
+ /** Track an explicitly read-only handler through completion, including client disconnects. */
28
+ runReadOnlyRequest<T>(res: Response, operation: () => T | Promise<T>): Promise<T>;
26
29
  getProcessName(): string;
27
30
  private getPublishedApiHost;
28
31
  /**
@@ -94,6 +94,10 @@ export default class UnsApiProxy extends UnsProxy {
94
94
  }
95
95
  setOwnershipGate(gate) { this.app.setOwnershipGate(gate); }
96
96
  async drainRequests() { await this.app.drainRequests(); }
97
+ /** Track an explicitly read-only handler through completion, including client disconnects. */
98
+ async runReadOnlyRequest(res, operation) {
99
+ return this.app.runReadOnlyRequest(res, operation);
100
+ }
97
101
  getProcessName() {
98
102
  return this.processName;
99
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uns-kit/api",
3
- "version": "3.0.38",
3
+ "version": "3.0.39",
4
4
  "description": "Express-powered API gateway plugin for UnsProxyProcess with JWT/JWKS support.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "multer": "^2.2.0",
38
38
  "hyparquet-writer": "^0.16.5",
39
39
  "uuid": "^14.0.1",
40
- "@uns-kit/core": "3.0.38"
40
+ "@uns-kit/core": "3.0.39"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/cookie-parser": "^1.4.10",