@mcp-abap-adt/connection 7.0.0 → 8.0.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 CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [8.0.0] - 2026-09-08
11
+
12
+ **A consumer holding the contract can now do what the connection could always do.**
13
+
14
+ ### Changed
15
+
16
+ - **BREAKING: onto `@mcp-abap-adt/interfaces@^39.0.0`**, from `^21.0.0`.
17
+ Seventeen majors, and the whole of it was eight compiler errors in one file.
18
+
19
+ - `makeAdtRequest` returns `IAdtWireResponse<T, D>`, not `IAdtResponse<T, D>`.
20
+ The two parted company when `IAdtResponse` became the result-or-failure
21
+ union a *member* answers, with `E extends IAdtError`; a connection answers
22
+ the raw HTTP shape, which is what `IAdtWireResponse` is. Six occurrences,
23
+ all of them the same rename — no behaviour moved.
24
+ - `isNetworkError` is this package's own again, in `src/utils/networkErrors.ts`.
25
+ `@mcp-abap-adt/interfaces` published it until 29.0.0, when that package
26
+ stopped emitting code: it holds types, interfaces and constants, and a
27
+ predicate is none of those. `NETWORK_ERROR_CODES` is still imported from
28
+ there — the codes are a constant, the judgement is not.
29
+
30
+ ### Added
31
+
32
+ - **`flushGoodbye(timeoutMs?)`** — waits for the goodbye `disconnect()`
33
+ dispatched, within a budget.
34
+
35
+ `disconnect()` sends the logoff and does not await it, deliberately: a goodbye
36
+ carries no request timeout, and a server that never answers must not hold a
37
+ teardown open. That is right for a teardown and wrong for a reconnect —
38
+ `disconnect()` then `connect()` opens the next session while the previous
39
+ one's goodbye is still being assembled, and the server keeps both.
40
+
41
+ Measured on E19 through `@mcp-abap-adt/adt-clients`, whose harness recycled
42
+ the session after each test: a new ABAP session every one to two seconds for a
43
+ whole run, none released, each living to its own thirty-minute idle timeout.
44
+
45
+ The budget bounds the waiting, not the overlap: finishes in time and there is
46
+ none, does not and the caller proceeds while the goodbye stays outstanding.
47
+
48
+ - **The connection declares the atoms it honours**: `ICriticalSection`,
49
+ `IRequestProfiling` and — through 39.0.0's addition to
50
+ `ISessionLifecycleAware` — `flushGoodbye`.
51
+
52
+ All four methods already existed here. What changed is that a consumer can
53
+ reach them: `AbapConnection` is `IAbapConnection`, so until now
54
+ `beginCriticalSection()`, `setProfilingRequest()` and `flushGoodbye()` were
55
+ reachable only by casting to the concrete class, which is the thing the
56
+ contracts package exists to make unnecessary.
57
+
58
+ A test narrows a connection to each atom from the base contract and calls
59
+ through it. Worth saying what that does and does not check: TypeScript is
60
+ structural, so removing an atom from the `implements` list changes nothing —
61
+ the class still has the methods. What the clause buys is the compiler
62
+ checking the class against the contract: removing `beginCriticalSection`
63
+ itself is `TS2420`, which is the failure worth having.
64
+
65
+
10
66
  ## [7.0.0] - 2026-09-08
11
67
 
12
68
  **Headers that belong to the request stop belonging to the session.**
@@ -1457,7 +1513,8 @@ const connection = createAbapConnection(config, logger);
1457
1513
  - JWT token refresh now properly handles connection errors (401/403 during initial connect)
1458
1514
  - Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
1459
1515
  - Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
1460
- [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v7.0.0...HEAD
1516
+ [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.0...HEAD
1517
+ [8.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v7.0.0...v8.0.0
1461
1518
  [7.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.1.0...v7.0.0
1462
1519
  [6.0.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.0...v6.0.1
1463
1520
  [6.1.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.1...v6.1.0
@@ -1,10 +1,10 @@
1
- import { type IAdtResponse, type ISessionLifecycleAware } from '@mcp-abap-adt/interfaces';
1
+ import { type IAdtWireResponse, type ICriticalSection, type IRequestProfiling, type ISessionLifecycleAware } from '@mcp-abap-adt/interfaces';
2
2
  import type { SapConfig } from '../config/sapConfig.js';
3
3
  import type { ILogger } from '../logger.js';
4
4
  import { SessionLifecycle } from '../session/SessionLifecycle.js';
5
5
  import type { AbapConnection, AbapRequestOptions } from './AbapConnection.js';
6
6
  import { type IAdtSessionContext, type IAdtTransport } from './IAdtTransport.js';
7
- declare abstract class AbstractAbapConnection implements AbapConnection, ISessionLifecycleAware {
7
+ declare abstract class AbstractAbapConnection implements AbapConnection, ISessionLifecycleAware, ICriticalSection, IRequestProfiling {
8
8
  private readonly config;
9
9
  /**
10
10
  * Required, and constructed by the caller.
@@ -53,6 +53,21 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
53
53
  * raises the effective timeout to CRITICAL_SECTION_TIMEOUT (a large ceiling)
54
54
  * so the request runs to completion instead of being interrupted.
55
55
  */
56
+ /**
57
+ * The last goodbye this connection dispatched, so a caller can wait for it.
58
+ *
59
+ * `disconnect()` sends the logoff without awaiting it, for the reason stated
60
+ * there. That is right for a teardown and wrong for a caller who is about to
61
+ * `connect()` again: the next session opens while the previous one's goodbye
62
+ * is still being assembled, and a caller who recycles in a loop leaves one
63
+ * abandoned session per iteration. Measured against E19 from
64
+ * `@mcp-abap-adt/adt-clients`: a session every 1-2 seconds for a whole test
65
+ * run, none of them released, each surviving to its 30-minute idle timeout.
66
+ *
67
+ * Keeping it costs nothing and is the only thing a caller can hold on to —
68
+ * before this, the promise was created and dropped on the floor.
69
+ */
70
+ private goodbye;
56
71
  private inCriticalSection;
57
72
  /** Reference count for nested beginCriticalSection()/endCriticalSection() pairs. */
58
73
  private criticalSectionDepth;
@@ -241,6 +256,22 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
241
256
  * timeout by design.
242
257
  */
243
258
  disconnect(): Promise<void>;
259
+ /**
260
+ * Wait for the last dispatched goodbye, bounded.
261
+ *
262
+ * For the caller who ends a session and immediately opens another. Without
263
+ * it the two overlap and the server keeps both; with it the previous session
264
+ * is gone — or known not to be going — before the next one is asked for.
265
+ *
266
+ * Bounded because the goodbye deliberately carries no request timeout: a
267
+ * server that never answers must not hold a teardown open, and it must not
268
+ * hold this open either. On expiry this returns rather than throwing, since
269
+ * "the goodbye has not arrived yet" is not a failure of the caller's next
270
+ * step — and the session it addressed will fall to the idle timeout anyway.
271
+ *
272
+ * Safe to call at any time: with nothing dispatched it returns immediately.
273
+ */
274
+ flushGoodbye(timeoutMs?: number): Promise<void>;
244
275
  isConnected(): boolean;
245
276
  /**
246
277
  * Fingerprint of the SAP-side session, or null when none is known.
@@ -396,7 +427,7 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
396
427
  protected sessionFingerprint(): Map<string, string>;
397
428
  getBaseUrl(): Promise<string>;
398
429
  getAuthHeaders(): Promise<Record<string, string>>;
399
- makeAdtRequest<T = any, D = any>(options: AbapRequestOptions): Promise<IAdtResponse<T, D>>;
430
+ makeAdtRequest<T = any, D = any>(options: AbapRequestOptions): Promise<IAdtWireResponse<T, D>>;
400
431
  private performRequest;
401
432
  protected abstract buildAuthorizationHeader(): string;
402
433
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ exports.AbstractAbapConnection = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const interfaces_1 = require("@mcp-abap-adt/interfaces");
6
6
  const SessionLifecycle_js_1 = require("../session/SessionLifecycle.js");
7
+ const networkErrors_js_1 = require("../utils/networkErrors.js");
7
8
  const timeouts_js_1 = require("../utils/timeouts.js");
8
9
  const csrfConfig_js_1 = require("./csrfConfig.js");
9
10
  const IAdtTransport_js_1 = require("./IAdtTransport.js");
@@ -81,6 +82,21 @@ class AbstractAbapConnection {
81
82
  * raises the effective timeout to CRITICAL_SECTION_TIMEOUT (a large ceiling)
82
83
  * so the request runs to completion instead of being interrupted.
83
84
  */
85
+ /**
86
+ * The last goodbye this connection dispatched, so a caller can wait for it.
87
+ *
88
+ * `disconnect()` sends the logoff without awaiting it, for the reason stated
89
+ * there. That is right for a teardown and wrong for a caller who is about to
90
+ * `connect()` again: the next session opens while the previous one's goodbye
91
+ * is still being assembled, and a caller who recycles in a loop leaves one
92
+ * abandoned session per iteration. Measured against E19 from
93
+ * `@mcp-abap-adt/adt-clients`: a session every 1-2 seconds for a whole test
94
+ * run, none of them released, each surviving to its 30-minute idle timeout.
95
+ *
96
+ * Keeping it costs nothing and is the only thing a caller can hold on to —
97
+ * before this, the promise was created and dropped on the floor.
98
+ */
99
+ goodbye = Promise.resolve();
84
100
  inCriticalSection = false;
85
101
  /** Reference count for nested beginCriticalSection()/endCriticalSection() pairs. */
86
102
  criticalSectionDepth = 0;
@@ -350,11 +366,47 @@ class AbstractAbapConnection {
350
366
  // The context is taken now, while the session is still true: the clear
351
367
  // below runs while the close is suspended on its first await.
352
368
  const context = this.sessionContext();
353
- const inFlight = Promise.resolve(this.transport.close(context)).then(() => undefined);
369
+ // Kept, not dropped: `flushGoodbye()` is how a caller who is about to
370
+ // reconnect waits for it. Rejections are absorbed here so that holding
371
+ // the promise never turns a failed goodbye into an unhandled rejection —
372
+ // the transport already logs what went wrong.
373
+ this.goodbye = Promise.resolve(this.transport.close(context)).then(() => undefined, () => undefined);
354
374
  this.clearSessionState();
355
375
  this.lifecycle.markDisconnected();
356
376
  });
357
377
  }
378
+ /**
379
+ * Wait for the last dispatched goodbye, bounded.
380
+ *
381
+ * For the caller who ends a session and immediately opens another. Without
382
+ * it the two overlap and the server keeps both; with it the previous session
383
+ * is gone — or known not to be going — before the next one is asked for.
384
+ *
385
+ * Bounded because the goodbye deliberately carries no request timeout: a
386
+ * server that never answers must not hold a teardown open, and it must not
387
+ * hold this open either. On expiry this returns rather than throwing, since
388
+ * "the goodbye has not arrived yet" is not a failure of the caller's next
389
+ * step — and the session it addressed will fall to the idle timeout anyway.
390
+ *
391
+ * Safe to call at any time: with nothing dispatched it returns immediately.
392
+ */
393
+ async flushGoodbye(timeoutMs = 5000) {
394
+ let timer;
395
+ try {
396
+ await Promise.race([
397
+ this.goodbye,
398
+ new Promise((resolve) => {
399
+ timer = setTimeout(resolve, timeoutMs);
400
+ // Never hold the process open for a goodbye nobody is waiting on.
401
+ timer.unref?.();
402
+ }),
403
+ ]);
404
+ }
405
+ finally {
406
+ if (timer)
407
+ clearTimeout(timer);
408
+ }
409
+ }
358
410
  isConnected() {
359
411
  return this.lifecycle.connected;
360
412
  }
@@ -927,7 +979,7 @@ class AbstractAbapConnection {
927
979
  }
928
980
  // Check if this is a network error (connection refused, timeout, DNS, etc.)
929
981
  // Don't retry for network errors - these indicate infrastructure/VPN issues
930
- const networkError = (0, interfaces_1.isNetworkError)(error);
982
+ const networkError = (0, networkErrors_js_1.isNetworkError)(error);
931
983
  if (networkError) {
932
984
  this.logger?.error(`Network error - cannot connect to SAP system: ${errorDetails.message}`, errorDetails);
933
985
  throw error;
@@ -0,0 +1,2 @@
1
+ export declare function isNetworkError(error: unknown): boolean;
2
+ //# sourceMappingURL=networkErrors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"networkErrors.d.ts","sourceRoot":"","sources":["../../src/utils/networkErrors.ts"],"names":[],"mappings":"AAkBA,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAItD"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isNetworkError = isNetworkError;
4
+ /**
5
+ * Whether an error is the network's rather than the server's.
6
+ *
7
+ * `@mcp-abap-adt/interfaces` published this as a function until 29.0.0, when
8
+ * that package stopped emitting code: it holds types, interfaces and constants,
9
+ * and a predicate is none of those. The codes are still there —
10
+ * {@link NETWORK_ERROR_CODES} — and the judgement lives here, with the wire it
11
+ * judges.
12
+ *
13
+ * The distinction is not cosmetic. A refusal from SAP is an answer and can be
14
+ * retried once a token is refreshed; `ECONNREFUSED` is a statement about
15
+ * infrastructure, and retrying it re-runs the same failure against the same
16
+ * unreachable host.
17
+ */
18
+ const interfaces_1 = require("@mcp-abap-adt/interfaces");
19
+ const CODES = new Set(Object.values(interfaces_1.NETWORK_ERROR_CODES));
20
+ function isNetworkError(error) {
21
+ if (!error || typeof error !== 'object')
22
+ return false;
23
+ const code = error.code;
24
+ return typeof code === 'string' && CODES.has(code);
25
+ }
@@ -294,7 +294,7 @@ session was negotiated elsewhere:
294
294
  ```typescript
295
295
  import type { IAuthProvider, IRenewableCredential } from '@mcp-abap-adt/interfaces';
296
296
 
297
- function isRenewable(c: IAuthProvider): c is IRenewableCredential {
297
+ function isRenewable(c: IAuthProvider): c is IAuthProvider & IRenewableCredential {
298
298
  return typeof (c as Partial<IRenewableCredential>).renew === 'function';
299
299
  }
300
300
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/connection",
3
- "version": "7.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "ABAP connection layer for MCP ABAP ADT server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -53,7 +53,7 @@
53
53
  "node": ">=18.0.0"
54
54
  },
55
55
  "dependencies": {
56
- "@mcp-abap-adt/interfaces": "^21.0.0",
56
+ "@mcp-abap-adt/interfaces": "^39.0.0",
57
57
  "axios": "^1.16.0",
58
58
  "commander": "^14.0.3",
59
59
  "open": "^11.0.0"