@mcp-abap-adt/connection 7.0.0 → 8.0.1
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 +80 -1
- package/README.md +3 -1
- package/dist/connection/AbstractAbapConnection.d.ts +34 -3
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +54 -2
- package/dist/utils/networkErrors.d.ts +2 -0
- package/dist/utils/networkErrors.d.ts.map +1 -0
- package/dist/utils/networkErrors.js +25 -0
- package/docs/INDEX.md +2 -0
- package/docs/MIGRATION-6.0.md +1 -1
- package/docs/MIGRATION-8.0.md +171 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +23 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [8.0.1] - 2026-09-08
|
|
11
|
+
|
|
12
|
+
**Documentation only — 7.0.0 and 8.0.0 shipped without their migration note.**
|
|
13
|
+
|
|
14
|
+
Both majors are already on npm, and `docs/` travels in the tarball, so the
|
|
15
|
+
installed package described the pre-7.0.0 connection: no request id, no
|
|
16
|
+
profiling, no `flushGoodbye`, and no word on what a consumer on 6.x must
|
|
17
|
+
change. This release carries the documentation those two releases owed and
|
|
18
|
+
changes no code.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- `docs/MIGRATION-8.0.md` (new): what a consumer on 6.x does about the
|
|
23
|
+
`IAdtWireResponse` return of `makeAdtRequest`, the capability atoms the
|
|
24
|
+
class now declares, and the two headers every request now carries.
|
|
25
|
+
- `docs/STATEFUL_SESSION_GUIDE.md`: the session type is the connection's —
|
|
26
|
+
`x-sap-adt-sessiontype: stateful` is added by the transport, not by a
|
|
27
|
+
caller's headers — and `flushGoodbye` is how a caller waits for the
|
|
28
|
+
goodbye.
|
|
29
|
+
- `README.md` and `docs/INDEX.md` point at both.
|
|
30
|
+
|
|
31
|
+
## [8.0.0] - 2026-09-08
|
|
32
|
+
|
|
33
|
+
**A consumer holding the contract can now do what the connection could always do.**
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **BREAKING: onto `@mcp-abap-adt/interfaces@^39.0.0`**, from `^21.0.0`.
|
|
38
|
+
Seventeen majors, and the whole of it was eight compiler errors in one file.
|
|
39
|
+
|
|
40
|
+
- `makeAdtRequest` returns `IAdtWireResponse<T, D>`, not `IAdtResponse<T, D>`.
|
|
41
|
+
The two parted company when `IAdtResponse` became the result-or-failure
|
|
42
|
+
union a *member* answers, with `E extends IAdtError`; a connection answers
|
|
43
|
+
the raw HTTP shape, which is what `IAdtWireResponse` is. Six occurrences,
|
|
44
|
+
all of them the same rename — no behaviour moved.
|
|
45
|
+
- `isNetworkError` is this package's own again, in `src/utils/networkErrors.ts`.
|
|
46
|
+
`@mcp-abap-adt/interfaces` published it until 29.0.0, when that package
|
|
47
|
+
stopped emitting code: it holds types, interfaces and constants, and a
|
|
48
|
+
predicate is none of those. `NETWORK_ERROR_CODES` is still imported from
|
|
49
|
+
there — the codes are a constant, the judgement is not.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`flushGoodbye(timeoutMs?)`** — waits for the goodbye `disconnect()`
|
|
54
|
+
dispatched, within a budget.
|
|
55
|
+
|
|
56
|
+
`disconnect()` sends the logoff and does not await it, deliberately: a goodbye
|
|
57
|
+
carries no request timeout, and a server that never answers must not hold a
|
|
58
|
+
teardown open. That is right for a teardown and wrong for a reconnect —
|
|
59
|
+
`disconnect()` then `connect()` opens the next session while the previous
|
|
60
|
+
one's goodbye is still being assembled, and the server keeps both.
|
|
61
|
+
|
|
62
|
+
Measured on E19 through `@mcp-abap-adt/adt-clients`, whose harness recycled
|
|
63
|
+
the session after each test: a new ABAP session every one to two seconds for a
|
|
64
|
+
whole run, none released, each living to its own thirty-minute idle timeout.
|
|
65
|
+
|
|
66
|
+
The budget bounds the waiting, not the overlap: finishes in time and there is
|
|
67
|
+
none, does not and the caller proceeds while the goodbye stays outstanding.
|
|
68
|
+
|
|
69
|
+
- **The connection declares the atoms it honours**: `ICriticalSection`,
|
|
70
|
+
`IRequestProfiling` and — through 39.0.0's addition to
|
|
71
|
+
`ISessionLifecycleAware` — `flushGoodbye`.
|
|
72
|
+
|
|
73
|
+
All four methods already existed here. What changed is that a consumer can
|
|
74
|
+
reach them: `AbapConnection` is `IAbapConnection`, so until now
|
|
75
|
+
`beginCriticalSection()`, `setProfilingRequest()` and `flushGoodbye()` were
|
|
76
|
+
reachable only by casting to the concrete class, which is the thing the
|
|
77
|
+
contracts package exists to make unnecessary.
|
|
78
|
+
|
|
79
|
+
A test narrows a connection to each atom from the base contract and calls
|
|
80
|
+
through it. Worth saying what that does and does not check: TypeScript is
|
|
81
|
+
structural, so removing an atom from the `implements` list changes nothing —
|
|
82
|
+
the class still has the methods. What the clause buys is the compiler
|
|
83
|
+
checking the class against the contract: removing `beginCriticalSection`
|
|
84
|
+
itself is `TS2420`, which is the failure worth having.
|
|
85
|
+
|
|
86
|
+
|
|
10
87
|
## [7.0.0] - 2026-09-08
|
|
11
88
|
|
|
12
89
|
**Headers that belong to the request stop belonging to the session.**
|
|
@@ -1457,7 +1534,9 @@ const connection = createAbapConnection(config, logger);
|
|
|
1457
1534
|
- JWT token refresh now properly handles connection errors (401/403 during initial connect)
|
|
1458
1535
|
- Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
|
|
1459
1536
|
- Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
|
|
1460
|
-
[Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/
|
|
1537
|
+
[Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.1...HEAD
|
|
1538
|
+
[8.0.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.0...v8.0.1
|
|
1539
|
+
[8.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v7.0.0...v8.0.0
|
|
1461
1540
|
[7.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.1.0...v7.0.0
|
|
1462
1541
|
[6.0.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.0...v6.0.1
|
|
1463
1542
|
[6.1.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.1...v6.1.0
|
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ The package uses a clean separation of concerns:
|
|
|
34
34
|
|
|
35
35
|
- **`AbstractAbapConnection`** (abstract, internal only):
|
|
36
36
|
- Common HTTP request logic
|
|
37
|
-
- Session lifecycle: `connect()` / `disconnect()`, admission, lock windows, teardown draining
|
|
37
|
+
- Session lifecycle: `connect()` / `disconnect()` / `flushGoodbye()`, admission, lock windows, teardown draining
|
|
38
|
+
- Capability atoms a consumer narrows to, rather than casting to a connector class:
|
|
39
|
+
`ISessionLifecycleAware`, `ICriticalSection`, `IRequestProfiling`
|
|
38
40
|
- Session management (cookies, CSRF tokens)
|
|
39
41
|
- CSRF token fetching with retry
|
|
40
42
|
- Auth-agnostic - knows nothing about Basic or JWT
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type
|
|
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<
|
|
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":"AAEA,OAAO,EAEL,KAAK,
|
|
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
|
-
|
|
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,
|
|
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 @@
|
|
|
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
|
+
}
|
package/docs/INDEX.md
CHANGED
|
@@ -15,6 +15,7 @@ mcp-abap-connection/
|
|
|
15
15
|
│ ├── INSTALLATION.md # Setup and installation guide
|
|
16
16
|
│ ├── USAGE.md # API documentation and examples
|
|
17
17
|
│ ├── MIGRATION-2.0.md # Moving to the explicit session lifecycle
|
|
18
|
+
│ ├── MIGRATION-8.0.md # request headers leave the stateful branch; onto interfaces 39; flushGoodbye
|
|
18
19
|
│ ├── MIGRATION-6.0.md # the factory and the per-credential classes go; RFC is a transport
|
|
19
20
|
│ ├── MIGRATION-4.0.md # JWT error classification: 401 refreshes, 403 propagates
|
|
20
21
|
│ ├── SCOPE.md # What this package does and does not own
|
|
@@ -44,6 +45,7 @@ mcp-abap-connection/
|
|
|
44
45
|
- 🔑 [JWT Auth Tools](./JWT_AUTH_TOOLS.md) - CLI tool for browser-based authentication
|
|
45
46
|
|
|
46
47
|
### Upgrading
|
|
48
|
+
- 🚚 [Migrating to 7.0.0 and 8.0.0](./MIGRATION-8.0.md) - `sap-adt-request-id` and `X-sap-adt-profiling` on every request, `x-sap-security-session: use` on cloud, the contracts floor at 39, and `flushGoodbye()`
|
|
47
49
|
- 🚚 [Migrating to 6.0.0](./MIGRATION-6.0.md) - the factory and the per-credential classes are removed; the RFC wire is a transport you hand to the on-prem connector
|
|
48
50
|
- 🧱 [Migrating to 4.0.0](./MIGRATION-4.0.md) - JWT error classification: a 401 refreshes, a 403 propagates with the server's message
|
|
49
51
|
- 🧱 [Migrating to 2.0.0](./MIGRATION-2.0.md) - The explicit session lifecycle: `connect()` is required
|
package/docs/MIGRATION-6.0.md
CHANGED
|
@@ -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
|
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Migration to 7.0 and 8.0
|
|
2
|
+
|
|
3
|
+
Two majors, and **no code of yours has to change for either**. One alters what
|
|
4
|
+
goes on the wire; the other moves the contracts floor and makes methods you
|
|
5
|
+
already had reachable through the types you already hold.
|
|
6
|
+
|
|
7
|
+
If you build against `@mcp-abap-adt/interfaces` and never cast to a connector
|
|
8
|
+
class, there is nothing to do but install.
|
|
9
|
+
|
|
10
|
+
## 7.0 — headers that belong to the request stop belonging to the session
|
|
11
|
+
|
|
12
|
+
Three headers used to be written together, inside the stateful branch:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
if (this.sessionMode === 'stateful') {
|
|
16
|
+
requestHeaders['x-sap-adt-sessiontype'] = 'stateful';
|
|
17
|
+
requestHeaders['sap-adt-request-id'] = randomUUID().replace(/-/g, '');
|
|
18
|
+
requestHeaders['X-sap-adt-profiling'] = 'server-time';
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Only the first belongs there. A request id identifies the **request**, and
|
|
23
|
+
asking the server to report its own processing time is not a property of the
|
|
24
|
+
session either. Eclipse sends both on everything — measured on ADT 3.60.0, a
|
|
25
|
+
stateless source `PUT` carries them and no session type at all.
|
|
26
|
+
|
|
27
|
+
It stayed invisible while every write ran inside a lock window. Once
|
|
28
|
+
`@mcp-abap-adt/adt-clients` narrowed stateful to the `LOCK` and the `UNLOCK`,
|
|
29
|
+
the writes silently lost two headers: of 792 requests in a full run, 99 carried
|
|
30
|
+
a request id and **693 carried neither**.
|
|
31
|
+
|
|
32
|
+
### What changed for you
|
|
33
|
+
|
|
34
|
+
| | |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `sap-adt-request-id` | now on every request, fresh each time |
|
|
37
|
+
| `X-sap-adt-profiling` | now on every request, from a settable default |
|
|
38
|
+
| `x-sap-security-session: use` | **cloud only**, on every request once a session exists |
|
|
39
|
+
| `x-sap-adt-sessiontype` | unchanged — still the only one that varies with the mode |
|
|
40
|
+
|
|
41
|
+
Nothing in the type surface moved. The major is for the wire: every request
|
|
42
|
+
looks different in an SAP trace, and a system that reacts badly to either should
|
|
43
|
+
be findable by version rather than by reading a dump.
|
|
44
|
+
|
|
45
|
+
### Your own headers win
|
|
46
|
+
|
|
47
|
+
Both new headers are **defaults**. A caller who names either in
|
|
48
|
+
`options.headers` keeps their value, matched without case:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await conn.makeAdtRequest({
|
|
52
|
+
url, method: 'GET', timeout: 30_000,
|
|
53
|
+
headers: { 'sap-adt-request-id': myCorrelationId }, // kept, not replaced
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That is not hypothetical: `adt-clients` passes its own id to
|
|
58
|
+
`getDiscovery({ requestId })` so the id it logs is the id on the wire.
|
|
59
|
+
|
|
60
|
+
### Turning the profiling off
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
conn.setProfilingRequest(null); // ask for nothing
|
|
64
|
+
conn.setProfilingRequest('server-time'); // the default, and what Eclipse asks for
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Nothing in this package reads the `server-time=…` that comes back. An
|
|
68
|
+
investigation does, though — the unit is **microseconds**, which a trial's
|
|
69
|
+
unpublish job settled by answering `server-time=132512547` on a request that
|
|
70
|
+
takes about 133 seconds.
|
|
71
|
+
|
|
72
|
+
## 8.0 — onto interfaces 39.0.0, and the atoms are declared
|
|
73
|
+
|
|
74
|
+
### The floor
|
|
75
|
+
|
|
76
|
+
`@mcp-abap-adt/interfaces` moves from `^21.0.0` to `^39.0.0`. Install it
|
|
77
|
+
alongside; a consumer pinned below 39 cannot have both.
|
|
78
|
+
|
|
79
|
+
Seventeen majors, and the whole migration inside this package was eight compiler
|
|
80
|
+
errors in one file — `makeAdtRequest` returning `IAdtWireResponse<T, D>` rather
|
|
81
|
+
than `IAdtResponse<T, D>`, and `isNetworkError` coming home because interfaces
|
|
82
|
+
stopped emitting code in 29.0.0.
|
|
83
|
+
|
|
84
|
+
**If you deduplicate nothing else, deduplicate this.** Two copies of the
|
|
85
|
+
contracts in one graph are structurally identical and do not compare equal, so
|
|
86
|
+
they produce errors that read as impossible.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
npm ls @mcp-abap-adt/interfaces # should print one version, deduped
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `flushGoodbye()` — the half of `disconnect()` that was missing
|
|
93
|
+
|
|
94
|
+
`disconnect()` dispatches the logoff and does not await it, on purpose: a
|
|
95
|
+
goodbye carries no request timeout, and a server that never answers must not
|
|
96
|
+
hold a teardown open.
|
|
97
|
+
|
|
98
|
+
That is right for a teardown and wrong for a **reconnect**:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await conn.disconnect();
|
|
102
|
+
await conn.flushGoodbye(); // give the goodbye its budget to finish first
|
|
103
|
+
await conn.connect();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Without the middle line, the next session opens while the previous one's goodbye
|
|
107
|
+
is still being assembled and the server keeps both. Measured on E19 through a
|
|
108
|
+
test harness that recycled the session after each test: **a new ABAP session
|
|
109
|
+
every one to two seconds for a whole run, none released**, each living to its
|
|
110
|
+
own thirty-minute idle timeout.
|
|
111
|
+
|
|
112
|
+
**The budget bounds the waiting, not the overlap.** If the goodbye finishes in
|
|
113
|
+
time there is no overlap; if it does not, you proceed and it stays outstanding
|
|
114
|
+
for as long as it takes. A return is not a confirmation and not even of
|
|
115
|
+
dispatch.
|
|
116
|
+
|
|
117
|
+
Calling `disconnect()` twice is **not** a substitute — a repeat call does not
|
|
118
|
+
wait either.
|
|
119
|
+
|
|
120
|
+
### Reaching the controls through the contract
|
|
121
|
+
|
|
122
|
+
`beginCriticalSection()`, `setProfilingRequest()` and `flushGoodbye()` all
|
|
123
|
+
existed before. What changed is that a consumer can reach them without a cast:
|
|
124
|
+
`AbapConnection` is `IAbapConnection`, and these now live on capability atoms
|
|
125
|
+
that this connection declares.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import type {
|
|
129
|
+
IAbapConnection,
|
|
130
|
+
ICriticalSection,
|
|
131
|
+
IRequestProfiling,
|
|
132
|
+
ISessionLifecycleAware,
|
|
133
|
+
} from '@mcp-abap-adt/interfaces';
|
|
134
|
+
|
|
135
|
+
function protectTheWindow(conn: IAbapConnection & ICriticalSection) {
|
|
136
|
+
conn.beginCriticalSection();
|
|
137
|
+
try {
|
|
138
|
+
// the ordinary per-request deadline does not apply in here
|
|
139
|
+
} finally {
|
|
140
|
+
conn.endCriticalSection();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`flushGoodbye` is a member of `ISessionLifecycleAware`, not an atom of its own:
|
|
146
|
+
waiting for the goodbye is not a separate capability from sending it.
|
|
147
|
+
|
|
148
|
+
### One thing to know about `implements`
|
|
149
|
+
|
|
150
|
+
TypeScript is structural. Removing an atom from a class's `implements` list
|
|
151
|
+
changes nothing for a consumer — the class still has the methods and narrowing
|
|
152
|
+
still succeeds. What the clause buys is the compiler checking the class *against*
|
|
153
|
+
the contract in the other direction: remove `beginCriticalSection` itself and it
|
|
154
|
+
is `TS2420`.
|
|
155
|
+
|
|
156
|
+
Worth knowing before writing a guard that tests the clause rather than the
|
|
157
|
+
method.
|
|
158
|
+
|
|
159
|
+
### `IRenewableCredential` is an atom now
|
|
160
|
+
|
|
161
|
+
Not this package's change, but it lands with the floor. Renewing is something a
|
|
162
|
+
credential can also do, not a kind of credential, so a guard that narrowed to
|
|
163
|
+
`IRenewableCredential` alone now hands the caller something that renews and
|
|
164
|
+
cannot authenticate:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// before
|
|
168
|
+
function isRenewable(c: IAuthProvider): c is IRenewableCredential
|
|
169
|
+
// after
|
|
170
|
+
function isRenewable(c: IAuthProvider): c is IAuthProvider & IRenewableCredential
|
|
171
|
+
```
|
|
@@ -120,6 +120,22 @@ authentication answer. A 403 never did this: it is an authorization answer, not
|
|
|
120
120
|
If you decide the refusal meant a stale token, `renew()` and reconnect are yours to call — and a
|
|
121
121
|
reconnect is a NEW session, so do it outside a lock window rather than inside one.
|
|
122
122
|
|
|
123
|
+
**Wait for the goodbye before opening the next one.** `disconnect()` dispatches the logoff and does
|
|
124
|
+
not await it, so a reconnect otherwise opens the next session while the previous one's goodbye is
|
|
125
|
+
still being assembled, and the server keeps both:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
await conn.disconnect();
|
|
129
|
+
await conn.flushGoodbye(); // give the goodbye its budget to finish first
|
|
130
|
+
await conn.connect();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Measured on E19 through a harness that recycled the session after each test: a new ABAP session
|
|
134
|
+
every one to two seconds for a whole run, none released, each living to its own thirty-minute idle
|
|
135
|
+
timeout. The budget bounds the waiting, not the overlap — if the goodbye finishes in time there is
|
|
136
|
+
none, and if it does not you proceed while it stays outstanding. Calling `disconnect()` again is not
|
|
137
|
+
a substitute: a repeat call does not wait either.
|
|
138
|
+
|
|
123
139
|
---
|
|
124
140
|
|
|
125
141
|
## Interaction With ADT Clients
|
|
@@ -189,6 +205,13 @@ lock is still very much held. That — not any teardown — is why
|
|
|
189
205
|
`beginCriticalSection()` raises the effective timeout to a large ceiling for the
|
|
190
206
|
duration of a `lock → modify → unlock` chain.
|
|
191
207
|
|
|
208
|
+
Since 8.0.0 you reach it through the contract rather than the class: it is
|
|
209
|
+
`ICriticalSection` in `@mcp-abap-adt/interfaces`, which this connection declares.
|
|
210
|
+
What it promises is narrow and worth stating exactly — inside a section the
|
|
211
|
+
*ordinary* per-request deadline does not apply. Not that no request can be cut
|
|
212
|
+
short: the ceiling is `SAP_TIMEOUT_CRITICAL`, ten minutes by default, and a
|
|
213
|
+
socket ends a request whatever a contract says.
|
|
214
|
+
|
|
192
215
|
### Two sessions, and they are not the same thing
|
|
193
216
|
|
|
194
217
|
There are two sessions here, and they are not the same thing:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/connection",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
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": "^
|
|
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"
|