@mcp-abap-adt/connection 9.3.1 → 9.3.3

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,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.3.3] - 2026-09-27
11
+
12
+ ### Changed
13
+
14
+ - **Over RFC, stateless calls reuse a conversation and reset its server
15
+ context when supported.** `@mcp-abap-adt/sap-rfc-lite` 0.2.0 adds
16
+ `resetServerContext()` (`RfcResetServerContext`), so each call gets a clean
17
+ ABAP context without logging in again. Measured on E19, this took about
18
+ 0.24 s per call, compared with 0.67 s when opening a conversation each time.
19
+ Concurrent calls use a separate conversation so they cannot run between a
20
+ previous call and its reset. If reset fails, that conversation is closed.
21
+ With `sap-rfc-lite` 0.1.x, calls continue to use a new conversation each.
22
+ Create, read after create, two lock/write/unlock rounds and delete passed on
23
+ one E19 connection.
24
+
25
+ ## [9.3.2] - 2026-09-27
26
+
27
+ ### Fixed
28
+
29
+ - **Over RFC, a request that is not stateful runs on a conversation of its
30
+ own.** `RfcTransport` carried everything on one conversation, one ABAP
31
+ session, so whatever a program left in it stayed:
32
+ - after a package was created or written, every further write or delete of
33
+ it answered 400 PAK/058 "Package … is already locked" (`CL_PACKAGE`'s
34
+ instance buffer);
35
+ - a read straight after a create answered 400 SADT_RESOURCE/007.
36
+
37
+ Now the conversation `open()` makes carries the requests marked `stateful`
38
+ (the lock and its release), and every other request opens one of its own
39
+ and closes it when it answers. That is Eclipse ADT's split over JCo: one
40
+ stateful enqueue session, every other call in a session of its own.
41
+ Measured on E19, one connection: create, read, two lock/PUT/unlock rounds
42
+ and a delete all pass. The price is a logon per non-stateful call — about
43
+ 0.5 s on E19, against about 0.1 s for the call itself.
44
+
45
+ Stateful is decided the same way on both wires (`isStatefulRequest`): the
46
+ connection's flag, or a session header the caller wrote itself. And a call
47
+ whose own conversation is still logging on when the wire is closed sends
48
+ nothing: it closes that conversation and refuses, rather than delivering a
49
+ `PUT` or `DELETE` on a wire already given back.
50
+
10
51
  ## [9.3.1] - 2026-09-27
11
52
 
12
53
  ### Fixed
@@ -1831,7 +1872,9 @@ const connection = createAbapConnection(config, logger);
1831
1872
  - JWT token refresh now properly handles connection errors (401/403 during initial connect)
1832
1873
  - Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
1833
1874
  - Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
1834
- [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.1...HEAD
1875
+ [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.3...HEAD
1876
+ [9.3.3]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.2...v9.3.3
1877
+ [9.3.2]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.1...v9.3.2
1835
1878
  [9.3.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.0...v9.3.1
1836
1879
  [9.3.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.2...v9.3.0
1837
1880
  [9.2.2]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.1...v9.2.2
package/README.md CHANGED
@@ -292,6 +292,14 @@ where stateful HTTP sessions are not usable: an RFC conversation is one ABAP
292
292
  session for its whole lifetime, which is the way past `423 invalid lock handle`
293
293
  on BASIS < 7.50.
294
294
 
295
+ Since 9.3.2, LOCK and UNLOCK in stateful mode share the persistent RFC
296
+ conversation; stateless requests use a separate ABAP context. With
297
+ `sap-rfc-lite` 0.2.0 or newer, that context is reset between calls on a kept
298
+ conversation; older clients open one conversation per call. This prevents stale
299
+ package state from causing PAK/058 on later writes or SADT_RESOURCE/007 on a
300
+ read after create. Concurrent calls use a separate conversation. See
301
+ [RFC conversation isolation](./docs/STATEFUL_SESSION_GUIDE.md#rfc-conversation-isolation-933).
302
+
295
303
  Needs the SAP NW RFC SDK on the machine and `npm install @mcp-abap-adt/sap-rfc-lite`.
296
304
 
297
305
  ```typescript
@@ -21,15 +21,7 @@
21
21
  import { type AgentOptions } from 'node:https';
22
22
  import type { ILogger } from '../logger.js';
23
23
  import type { IAdtEstablishContext, IAdtSessionContext, IAdtTransport, IAdtTransportRequest, IAdtTransportResponse } from './IAdtTransport.js';
24
- /**
25
- * Whether the request runs in the stateful session: marked so by the
26
- * connection, or — kept for callers that still write it themselves — carrying
27
- * the session header already.
28
- *
29
- * Exported for the wires that rewrite a request before sending it: the
30
- * verdict has to be taken on the request as the caller made it.
31
- */
32
- export declare function isStatefulRequest(request: IAdtTransportRequest): boolean;
24
+ export { isStatefulRequest } from './statefulRequest.js';
33
25
  export declare class HttpTransport implements IAdtTransport {
34
26
  private readonly agentOptions;
35
27
  protected readonly logger: ILogger | null;
@@ -1 +1 @@
1
- {"version":3,"file":"HttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/HttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAwC5B;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAaxE;AASD,qBAAa,aAAc,YAAW,aAAa;IA6B/C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IACzC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IApC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAuB;gBAGjB,YAAY,GAAE,MAAM,YAAyB,EAC3C,MAAM,GAAE,OAAO,GAAG,IAAW;IAChD;;;;;OAKG;IACc,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAGtE;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgC/C,0EAA0E;IAC1E,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB;;;;;;OAMG;IACH,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;;;OAOG;IACH,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASzC;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIlD;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmG7D,uEAAuE;IACvE,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI1C,4EAA4E;IAC5E,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,KAAK;IA6Bb;;;;;OAKG;IACH,SAAS,CAAC,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAItD,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;IAuBd;;;;;;OAMG;IACG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD;;;;;;OAMG;IACG,KAAK,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAExD;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIzE;;;;;;;;;;;;;OAaG;cACa,YAAY,CAC1B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAInB,QAAQ;CA4BvB"}
1
+ {"version":3,"file":"HttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/HttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAuC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AASzD,qBAAa,aAAc,YAAW,aAAa;IA6B/C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IACzC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IApC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAuB;gBAGjB,YAAY,GAAE,MAAM,YAAyB,EAC3C,MAAM,GAAE,OAAO,GAAG,IAAW;IAChD;;;;;OAKG;IACc,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAGtE;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgC/C,0EAA0E;IAC1E,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB;;;;;;OAMG;IACH,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;;;OAOG;IACH,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASzC;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIlD;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmG7D,uEAAuE;IACvE,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI1C,4EAA4E;IAC5E,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,KAAK;IA6Bb;;;;;OAKG;IACH,SAAS,CAAC,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAItD,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;IAuBd;;;;;;OAMG;IACG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD;;;;;;OAMG;IACG,KAAK,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAExD;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIzE;;;;;;;;;;;;;OAaG;cACa,YAAY,CAC1B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAInB,QAAQ;CA4BvB"}
@@ -23,12 +23,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  return (mod && mod.__esModule) ? mod : { "default": mod };
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.HttpTransport = void 0;
27
- exports.isStatefulRequest = isStatefulRequest;
26
+ exports.HttpTransport = exports.isStatefulRequest = void 0;
28
27
  const node_https_1 = require("node:https");
29
28
  const axios_1 = __importDefault(require("axios"));
30
29
  const cookies_js_1 = require("../utils/cookies.js");
31
30
  const csrfConfig_js_1 = require("./csrfConfig.js");
31
+ const statefulRequest_js_1 = require("./statefulRequest.js");
32
32
  /** The cookie that names a stateful ABAP context. */
33
33
  const CONTEXT_COOKIE = 'sap-contextid';
34
34
  /**
@@ -63,30 +63,9 @@ function withoutContextCookie(cookie) {
63
63
  .join('; ');
64
64
  return kept || undefined;
65
65
  }
66
- /** The header that asks SAP for the stateful session over HTTP. */
67
- const SESSION_TYPE_HEADER = 'x-sap-adt-sessiontype';
68
- /**
69
- * Whether the request runs in the stateful session: marked so by the
70
- * connection, or — kept for callers that still write it themselves — carrying
71
- * the session header already.
72
- *
73
- * Exported for the wires that rewrite a request before sending it: the
74
- * verdict has to be taken on the request as the caller made it.
75
- */
76
- function isStatefulRequest(request) {
77
- if (request.stateful)
78
- return true;
79
- const headers = request.headers;
80
- if (!headers)
81
- return false;
82
- for (const [name, value] of Object.entries(headers)) {
83
- if (name.toLowerCase() === SESSION_TYPE_HEADER &&
84
- String(value).toLowerCase() === 'stateful') {
85
- return true;
86
- }
87
- }
88
- return false;
89
- }
66
+ // The one verdict every wire takes; re-exported for the wires built on this one.
67
+ var statefulRequest_js_2 = require("./statefulRequest.js");
68
+ Object.defineProperty(exports, "isStatefulRequest", { enumerable: true, get: function () { return statefulRequest_js_2.isStatefulRequest; } });
90
69
  /** A 404 there means the system has no such endpoint, not that it is unwell. */
91
70
  function absentEndpoint(error) {
92
71
  const status = error
@@ -368,7 +347,7 @@ class HttpTransport {
368
347
  */
369
348
  dress(request) {
370
349
  const headers = request.headers;
371
- const stateful = isStatefulRequest(request);
350
+ const stateful = (0, statefulRequest_js_1.isStatefulRequest)(request);
372
351
  const dressed = {
373
352
  ...this.affinityHeaders(),
374
353
  ...this.sessionHeaders(),
@@ -404,7 +383,7 @@ class HttpTransport {
404
383
  * `LegacyOnPremHttpTransport`).
405
384
  */
406
385
  sessionTypeHeaders() {
407
- return { [SESSION_TYPE_HEADER]: 'stateful' };
386
+ return { [statefulRequest_js_1.SESSION_TYPE_HEADER]: 'stateful' };
408
387
  }
409
388
  /** A path becomes an address; anything already absolute is left alone. */
410
389
  address(url) {
@@ -1 +1 @@
1
- {"version":3,"file":"LegacyOnPremHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/LegacyOnPremHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EACV,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAI/D,qBAAa,yBAA0B,SAAQ,mBAAmB;IAChE,SAAkB,IAAI,wBAAwB;IAE9C,mFAAmF;cAChE,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI/D;;;;;;;;;OASG;IACY,IAAI,CACjB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAUjC,OAAO,CAAC,kBAAkB;IAY1B;;;;;;;OAOG;IACM,kBAAkB,IAAI,OAAO;CAGvC"}
1
+ {"version":3,"file":"LegacyOnPremHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/LegacyOnPremHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAK/D,qBAAa,yBAA0B,SAAQ,mBAAmB;IAChE,SAAkB,IAAI,wBAAwB;IAE9C,mFAAmF;cAChE,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI/D;;;;;;;;;OASG;IACY,IAAI,CACjB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAUjC,OAAO,CAAC,kBAAkB;IAY1B;;;;;;;OAOG;IACM,kBAAkB,IAAI,OAAO;CAGvC"}
@@ -15,8 +15,8 @@
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.LegacyOnPremHttpTransport = void 0;
18
- const HttpTransport_js_1 = require("./HttpTransport.js");
19
18
  const OnPremHttpTransport_js_1 = require("./OnPremHttpTransport.js");
19
+ const statefulRequest_js_1 = require("./statefulRequest.js");
20
20
  const SESSION_TYPE_HEADER = 'x-sap-adt-sessiontype';
21
21
  class LegacyOnPremHttpTransport extends OnPremHttpTransport_js_1.OnPremHttpTransport {
22
22
  kind = 'onprem-http-legacy';
@@ -40,7 +40,7 @@ class LegacyOnPremHttpTransport extends OnPremHttpTransport_js_1.OnPremHttpTrans
40
40
  // cookie, only without the header this system is hurt by.
41
41
  return super.send({
42
42
  ...this.withoutSessionType(request),
43
- stateful: (0, HttpTransport_js_1.isStatefulRequest)(request),
43
+ stateful: (0, statefulRequest_js_1.isStatefulRequest)(request),
44
44
  });
45
45
  }
46
46
  withoutSessionType(request) {
@@ -11,12 +11,32 @@
11
11
  * "invalid lock handle" on legacy systems (BASIS < 7.50) where stateful HTTP
12
12
  * sessions are not usable.
13
13
  *
14
+ * **Two conversations, the way Eclipse ADT holds them over JCo.** Its trace
15
+ * shows one long-lived stateful session (`JCoEnqueueSystemSession`) for `LOCK`
16
+ * and `UNLOCK`, and every other call — the `PUT` included — in a session of
17
+ * its own. Here:
18
+ *
19
+ * - the persistent conversation, opened by `open()` and kept while this wire
20
+ * lives, carries the requests marked `stateful` — the lock and what must
21
+ * share its session;
22
+ * - every other request opens a conversation of its own, and closes it when
23
+ * it answers.
24
+ *
25
+ * One conversation for everything kept whatever an ABAP program left in the
26
+ * session. `CL_PACKAGE` buffers its instances for the life of the session and
27
+ * a save leaves them changeable, so after a create or a write every further
28
+ * write or delete of that package answered 400 PAK/058 "Package … is already
29
+ * locked"; a read straight after a create answered 400 SADT_RESOURCE/007.
30
+ * Measured on E19 (2026-09-27): LOCK and UNLOCK on one conversation, the PUT
31
+ * on another — two writes in a row fail on a shared second conversation and
32
+ * pass on a fresh one per call. The price is a logon per non-stateful call
33
+ * (~0.5 s on E19, against ~0.1 s for the call itself).
34
+ *
14
35
  * **What is NOT here.** Cookies, the CSRF token and the session lifecycle
15
36
  * belong to the connection above this seam and are already in
16
37
  * `request.headers` by the time `send()` is called. `x-sap-adt-sessiontype` is
17
- * not sent at all: `request.stateful` is HTTP's to express, and over RFC
18
- * Eclipse's JCo trace carries no session header — its stateful session is a
19
- * connection of its own, not a header on the request. A transport that
38
+ * not sent at all: over RFC `request.stateful` picks the conversation, and
39
+ * Eclipse's JCo trace carries no session header either. A transport that
20
40
  * also captured cookies would be doing that work twice, and the two copies
21
41
  * would disagree the first time one of them was cleared.
22
42
  */
@@ -28,6 +48,13 @@ export interface IRfcConversation {
28
48
  close(): Promise<void>;
29
49
  call(fm: string, params: Record<string, unknown>): Promise<Record<string, any>>;
30
50
  readonly alive: boolean;
51
+ /**
52
+ * Ends the ABAP session context and keeps the connection open
53
+ * (`RfcResetServerContext`, `@mcp-abap-adt/sap-rfc-lite` >= 0.2.0). Where it
54
+ * exists, one conversation serves the non-stateful calls, reset after each;
55
+ * where it does not, each such call opens a conversation of its own.
56
+ */
57
+ resetServerContext?(): Promise<void>;
31
58
  }
32
59
  /** What this wire does beyond carrying the request. */
33
60
  export interface IRfcTransportOptions {
@@ -56,6 +83,23 @@ export declare class RfcTransport implements IOnPremTransport {
56
83
  /** Which system this wire is for. Read by the compiler, never at runtime. */
57
84
  readonly system: "onprem";
58
85
  private conversation;
86
+ /**
87
+ * Bumped by every `close()`. A call that opened a conversation of its own
88
+ * compares it after the logon: the wire may have been closed while that
89
+ * logon was under way, and a write must not go out on a wire the caller has
90
+ * already given back.
91
+ */
92
+ private generation;
93
+ /**
94
+ * The conversation the non-stateful calls share when the client can reset
95
+ * it: opened on the first such call, reset after every call, closed with the
96
+ * wire. `null` until then, or after a reset failed.
97
+ */
98
+ private stateless;
99
+ /** Taken by the call on `stateless` until its reset is done. */
100
+ private statelessBusy;
101
+ /** `false` once a client showed it cannot reset; then every call opens its own. */
102
+ private canReset;
59
103
  /** Names the conversation, and so the ABAP session it carries. */
60
104
  private conversationId;
61
105
  /**
@@ -110,5 +154,31 @@ export declare class RfcTransport implements IOnPremTransport {
110
154
  /** Never throws, and a repeat call finds nothing owed. */
111
155
  close(): Promise<void>;
112
156
  send(request: IAdtTransportRequest): Promise<IAdtTransportResponse>;
157
+ /**
158
+ * A request that is not stateful, in a fresh ABAP session.
159
+ *
160
+ * Where the client can reset (`resetServerContext`), one kept conversation
161
+ * serves these calls and is reset after each: a fresh context without a new
162
+ * logon, ~0.1 s instead of ~0.5 s on E19. Where it cannot, or while that
163
+ * conversation is busy with another call, the call opens a conversation of
164
+ * its own and closes it when it answers.
165
+ *
166
+ * A call and its reset go together: a second call on the kept conversation
167
+ * before the first one's reset would run in the first one's context — a
168
+ * package the first one saved would refuse the second one's write (PAK/058).
169
+ * So a concurrent call takes a conversation of its own instead of waiting.
170
+ */
171
+ private carryOnItsOwn;
172
+ private carryOnKept;
173
+ /** A conversation opened for this one call, closed when it answers. */
174
+ private carryOnThrowaway;
175
+ /**
176
+ * A new conversation, logged on. The logon takes time (~0.5 s on E19); if the
177
+ * wire was closed meanwhile, the conversation is closed again and nothing is
178
+ * sent — not a PUT, not a DELETE — on a wire already given back.
179
+ */
180
+ private openOwn;
181
+ private closeQuietly;
182
+ private carry;
113
183
  }
114
184
  //# sourceMappingURL=RfcTransport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EACV,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAgFD,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD,qBAAa,YAAa,YAAW,gBAAgB;IA+EjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhF1B,QAAQ,CAAC,IAAI,SAAS;IAEtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,YAAY,CAAiC;IACrD,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAM;IAE5B;;;;;;OAMG;IACH,MAAM,IAAI,IAAI;IAEd,2CAA2C;IAC3C,OAAO,IAAI,IAAI;IAIf;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9D,+BAA+B;IAC/B,SAAS,IAAI,IAAI;IAIjB;;;OAGG;IACH,cAAc,IAAI,IAAI;IAEtB;;;OAGG;IACH,6EAA6E;IAC7E,kBAAkB,IAAI,OAAO;IAI7B,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC,2EAA2E;IAC3E,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIzC;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAErB;;;;;OAKG;gBAEgB,OAAO,EAAE,MAAM,gBAAgB,EAC/B,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,OAAO,GAAE,oBAAyB;IAGrD,oDAAoD;IACpD,OAAO,CAAC,OAAO;IAcT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,0DAA0D;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAkK1E"}
1
+ {"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EACV,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAG5B,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;OAKG;IACH,kBAAkB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAgFD,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD,qBAAa,YAAa,YAAW,gBAAgB;IAiGjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAlG1B,QAAQ,CAAC,IAAI,SAAS;IAEtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,YAAY,CAAiC;IACrD;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAK;IAEvB;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAiC;IAClD,gEAAgE;IAChE,OAAO,CAAC,aAAa,CAAS;IAC9B,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAsB;IACtC,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAM;IAE5B;;;;;;OAMG;IACH,MAAM,IAAI,IAAI;IAEd,2CAA2C;IAC3C,OAAO,IAAI,IAAI;IAIf;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9D,+BAA+B;IAC/B,SAAS,IAAI,IAAI;IAIjB;;;OAGG;IACH,cAAc,IAAI,IAAI;IAEtB;;;OAGG;IACH,6EAA6E;IAC7E,kBAAkB,IAAI,OAAO;IAI7B,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC,2EAA2E;IAC3E,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIzC;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAErB;;;;;OAKG;gBAEgB,OAAO,EAAE,MAAM,gBAAgB,EAC/B,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,OAAO,GAAE,oBAAyB;IAGrD,oDAAoD;IACpD,OAAO,CAAC,OAAO;IAcT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B,0DAA0D;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBtB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAezE;;;;;;;;;;;;;OAaG;YACW,aAAa;YAUb,WAAW;IAqDzB,uEAAuE;YACzD,gBAAgB;IAa9B;;;;OAIG;YACW,OAAO;YAgBP,YAAY;YAQZ,KAAK;CAyKpB"}
@@ -12,18 +12,39 @@
12
12
  * "invalid lock handle" on legacy systems (BASIS < 7.50) where stateful HTTP
13
13
  * sessions are not usable.
14
14
  *
15
+ * **Two conversations, the way Eclipse ADT holds them over JCo.** Its trace
16
+ * shows one long-lived stateful session (`JCoEnqueueSystemSession`) for `LOCK`
17
+ * and `UNLOCK`, and every other call — the `PUT` included — in a session of
18
+ * its own. Here:
19
+ *
20
+ * - the persistent conversation, opened by `open()` and kept while this wire
21
+ * lives, carries the requests marked `stateful` — the lock and what must
22
+ * share its session;
23
+ * - every other request opens a conversation of its own, and closes it when
24
+ * it answers.
25
+ *
26
+ * One conversation for everything kept whatever an ABAP program left in the
27
+ * session. `CL_PACKAGE` buffers its instances for the life of the session and
28
+ * a save leaves them changeable, so after a create or a write every further
29
+ * write or delete of that package answered 400 PAK/058 "Package … is already
30
+ * locked"; a read straight after a create answered 400 SADT_RESOURCE/007.
31
+ * Measured on E19 (2026-09-27): LOCK and UNLOCK on one conversation, the PUT
32
+ * on another — two writes in a row fail on a shared second conversation and
33
+ * pass on a fresh one per call. The price is a logon per non-stateful call
34
+ * (~0.5 s on E19, against ~0.1 s for the call itself).
35
+ *
15
36
  * **What is NOT here.** Cookies, the CSRF token and the session lifecycle
16
37
  * belong to the connection above this seam and are already in
17
38
  * `request.headers` by the time `send()` is called. `x-sap-adt-sessiontype` is
18
- * not sent at all: `request.stateful` is HTTP's to express, and over RFC
19
- * Eclipse's JCo trace carries no session header — its stateful session is a
20
- * connection of its own, not a header on the request. A transport that
39
+ * not sent at all: over RFC `request.stateful` picks the conversation, and
40
+ * Eclipse's JCo trace carries no session header either. A transport that
21
41
  * also captured cookies would be doing that work twice, and the two copies
22
42
  * would disagree the first time one of them was cleared.
23
43
  */
24
44
  Object.defineProperty(exports, "__esModule", { value: true });
25
45
  exports.RfcTransport = void 0;
26
46
  const node_crypto_1 = require("node:crypto");
47
+ const statefulRequest_js_1 = require("./statefulRequest.js");
27
48
  /**
28
49
  * ADT exception types, mapped to what they would have been over HTTP. Standard
29
50
  * types, returned in `<exc:exception>` XML.
@@ -101,6 +122,23 @@ class RfcTransport {
101
122
  /** Which system this wire is for. Read by the compiler, never at runtime. */
102
123
  system = 'onprem';
103
124
  conversation = null;
125
+ /**
126
+ * Bumped by every `close()`. A call that opened a conversation of its own
127
+ * compares it after the logon: the wire may have been closed while that
128
+ * logon was under way, and a write must not go out on a wire the caller has
129
+ * already given back.
130
+ */
131
+ generation = 0;
132
+ /**
133
+ * The conversation the non-stateful calls share when the client can reset
134
+ * it: opened on the first such call, reset after every call, closed with the
135
+ * wire. `null` until then, or after a reset failed.
136
+ */
137
+ stateless = null;
138
+ /** Taken by the call on `stateless` until its reset is done. */
139
+ statelessBusy = false;
140
+ /** `false` once a client showed it cannot reset; then every call opens its own. */
141
+ canReset;
104
142
  /** Names the conversation, and so the ABAP session it carries. */
105
143
  conversationId = '';
106
144
  /**
@@ -182,6 +220,7 @@ class RfcTransport {
182
220
  async open() {
183
221
  if (this.conversation?.alive)
184
222
  return;
223
+ const generation = this.generation;
185
224
  const conversation = this.connect();
186
225
  try {
187
226
  await conversation.open();
@@ -189,21 +228,39 @@ class RfcTransport {
189
228
  catch (e) {
190
229
  throw new Error(`Failed to open RFC connection: ${message(e)}`);
191
230
  }
231
+ // close() ran while this logged on: the session it would have handed out
232
+ // belongs to a wire already given back, so it is closed, not kept.
233
+ if (generation !== this.generation) {
234
+ await this.closeQuietly(conversation);
235
+ throw new Error('RFC transport was closed while it was being opened.');
236
+ }
192
237
  this.conversation = conversation;
193
238
  // Minted here, where the session begins. A conversation opened after an
194
239
  // earlier one closed is a DIFFERENT ABAP session, and the fingerprint has
195
240
  // to say so — a constant would report `unchanged` across a reconnect and
196
241
  // hide exactly the replacement the identity policy exists to catch.
197
242
  this.conversationId = (0, node_crypto_1.randomUUID)();
198
- this.logger?.debug('RFC conversation opened (stateful by nature)');
243
+ this.logger?.debug('RFC conversation opened (the stateful one)');
199
244
  }
200
245
  /** Never throws, and a repeat call finds nothing owed. */
201
246
  async close() {
247
+ // Everything the wire holds is given up BEFORE the first await. Closing
248
+ // takes time, and a send() that ran meanwhile would otherwise find the
249
+ // old stateful conversation still `alive` — a lock request carried on a
250
+ // session already being given back — or open a stateless one of its own
251
+ // and send a write after close(). With both references gone and the
252
+ // generation moved on, such a send() is refused as "not open", and a
253
+ // logon still under way sees the change and sends nothing.
254
+ this.generation += 1;
255
+ const stateless = this.stateless;
202
256
  const conversation = this.conversation;
203
- if (!conversation)
204
- return;
257
+ this.stateless = null;
205
258
  this.conversation = null;
206
259
  this.conversationId = '';
260
+ if (stateless)
261
+ await this.closeQuietly(stateless);
262
+ if (!conversation)
263
+ return;
207
264
  try {
208
265
  await conversation.close();
209
266
  this.logger?.debug('RFC conversation closed');
@@ -216,6 +273,123 @@ class RfcTransport {
216
273
  if (!this.conversation?.alive) {
217
274
  throw new Error('RFC transport is not open. Call connect() first.');
218
275
  }
276
+ // The same verdict HTTP takes: the flag the connection sets, or a session
277
+ // header the caller wrote itself. A caller that asks for the lock's
278
+ // session either way has to land on the conversation that holds it.
279
+ // Taken once, here: every await below compares against it, and a call
280
+ // that finds the wire closed since it began sends nothing.
281
+ const generation = this.generation;
282
+ return (0, statefulRequest_js_1.isStatefulRequest)(request)
283
+ ? this.carry(request, this.conversation, generation)
284
+ : this.carryOnItsOwn(request, generation);
285
+ }
286
+ /**
287
+ * A request that is not stateful, in a fresh ABAP session.
288
+ *
289
+ * Where the client can reset (`resetServerContext`), one kept conversation
290
+ * serves these calls and is reset after each: a fresh context without a new
291
+ * logon, ~0.1 s instead of ~0.5 s on E19. Where it cannot, or while that
292
+ * conversation is busy with another call, the call opens a conversation of
293
+ * its own and closes it when it answers.
294
+ *
295
+ * A call and its reset go together: a second call on the kept conversation
296
+ * before the first one's reset would run in the first one's context — a
297
+ * package the first one saved would refuse the second one's write (PAK/058).
298
+ * So a concurrent call takes a conversation of its own instead of waiting.
299
+ */
300
+ async carryOnItsOwn(request, generation) {
301
+ if (this.canReset !== false && !this.statelessBusy) {
302
+ return this.carryOnKept(request, generation);
303
+ }
304
+ return this.carryOnThrowaway(request, generation);
305
+ }
306
+ async carryOnKept(request, generation) {
307
+ this.statelessBusy = true;
308
+ try {
309
+ if (!this.stateless?.alive) {
310
+ const opened = await this.openOwn(generation);
311
+ // Checked again after the await: close() may have run since openOwn()
312
+ // checked, and a conversation of a closed wire must not become the
313
+ // kept one of the next.
314
+ if (generation !== this.generation) {
315
+ await this.closeQuietly(opened);
316
+ throw new Error('RFC transport was closed while the call was being opened; nothing was sent.');
317
+ }
318
+ if (typeof opened.resetServerContext !== 'function') {
319
+ // An older client: it cannot be reset, so it is used once and closed,
320
+ // and every later call opens its own.
321
+ this.canReset = false;
322
+ try {
323
+ return await this.carry(request, opened, generation);
324
+ }
325
+ finally {
326
+ await this.closeQuietly(opened);
327
+ }
328
+ }
329
+ this.canReset = true;
330
+ this.stateless = opened;
331
+ this.logger?.debug('RFC stateless conversation opened (reset per call)');
332
+ }
333
+ const kept = this.stateless;
334
+ try {
335
+ return await this.carry(request, kept, generation);
336
+ }
337
+ finally {
338
+ try {
339
+ await kept.resetServerContext?.();
340
+ }
341
+ catch (e) {
342
+ // A context that could not be reset must not serve the next call.
343
+ this.logger?.debug(`RFC reset failed, dropping the conversation: ${message(e)}`);
344
+ if (this.stateless === kept)
345
+ this.stateless = null;
346
+ await this.closeQuietly(kept);
347
+ }
348
+ }
349
+ }
350
+ finally {
351
+ this.statelessBusy = false;
352
+ }
353
+ }
354
+ /** A conversation opened for this one call, closed when it answers. */
355
+ async carryOnThrowaway(request, generation) {
356
+ const own = await this.openOwn(generation);
357
+ this.logger?.debug('RFC conversation opened for one stateless call');
358
+ try {
359
+ return await this.carry(request, own, generation);
360
+ }
361
+ finally {
362
+ await this.closeQuietly(own);
363
+ }
364
+ }
365
+ /**
366
+ * A new conversation, logged on. The logon takes time (~0.5 s on E19); if the
367
+ * wire was closed meanwhile, the conversation is closed again and nothing is
368
+ * sent — not a PUT, not a DELETE — on a wire already given back.
369
+ */
370
+ async openOwn(generation) {
371
+ const own = this.connect();
372
+ try {
373
+ await own.open();
374
+ }
375
+ catch (e) {
376
+ throw new Error(`Failed to open RFC connection: ${message(e)}`);
377
+ }
378
+ if (generation !== this.generation || !this.conversation?.alive) {
379
+ await this.closeQuietly(own);
380
+ throw new Error('RFC transport was closed while the call was being opened; nothing was sent.');
381
+ }
382
+ return own;
383
+ }
384
+ async closeQuietly(conversation) {
385
+ try {
386
+ await conversation.close();
387
+ }
388
+ catch (e) {
389
+ this.logger?.debug(`RFC close error: ${message(e)}`);
390
+ }
391
+ }
392
+ async carry(request, conversation, generation) {
219
393
  const method = request.method.toUpperCase();
220
394
  // HTTP clients serialise `params`; RFC has no such step, so they go into
221
395
  // the URI here or they do not travel at all.
@@ -271,9 +445,14 @@ class RfcTransport {
271
445
  // mid-flight is precisely what the connection emulates over HTTP, where
272
446
  // a critical section raises the caller's timeout to a ten-minute ceiling so
273
447
  // an abort cannot orphan a lock handle. Here that comes free.
448
+ // The last point before anything leaves: nothing awaits between this
449
+ // check and the call, so a close() that ran at any await above stops it.
450
+ if (generation !== this.generation) {
451
+ throw new Error('RFC transport was closed before the call was sent; nothing was sent.');
452
+ }
274
453
  let raw;
275
454
  try {
276
- raw = await this.conversation.call('SADT_REST_RFC_ENDPOINT', {
455
+ raw = await conversation.call('SADT_REST_RFC_ENDPOINT', {
277
456
  REQUEST: {
278
457
  REQUEST_LINE: {
279
458
  METHOD: method,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Whether a request runs in the stateful session — the one a lock is taken and
3
+ * released in.
4
+ *
5
+ * One verdict for every wire. HTTP expresses it as a header and the context
6
+ * cookie, RFC as the conversation it picks; if the two wires decided it
7
+ * differently, a caller that asked for the session one way would keep its lock
8
+ * on one wire and lose it on the other.
9
+ */
10
+ import type { IAdtTransportRequest } from './IAdtTransport.js';
11
+ /** The header that asks SAP for the stateful session over HTTP. */
12
+ export declare const SESSION_TYPE_HEADER = "x-sap-adt-sessiontype";
13
+ /**
14
+ * Marked so by the connection, or — kept for callers that still write it
15
+ * themselves — carrying the session header already. Taken on the request as
16
+ * the caller made it, before a wire rewrites it.
17
+ */
18
+ export declare function isStatefulRequest(request: IAdtTransportRequest): boolean;
19
+ //# sourceMappingURL=statefulRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statefulRequest.d.ts","sourceRoot":"","sources":["../../src/connection/statefulRequest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAE3D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAaxE"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_TYPE_HEADER = void 0;
4
+ exports.isStatefulRequest = isStatefulRequest;
5
+ /** The header that asks SAP for the stateful session over HTTP. */
6
+ exports.SESSION_TYPE_HEADER = 'x-sap-adt-sessiontype';
7
+ /**
8
+ * Marked so by the connection, or — kept for callers that still write it
9
+ * themselves — carrying the session header already. Taken on the request as
10
+ * the caller made it, before a wire rewrites it.
11
+ */
12
+ function isStatefulRequest(request) {
13
+ if (request.stateful)
14
+ return true;
15
+ const headers = request.headers;
16
+ if (!headers)
17
+ return false;
18
+ for (const [name, value] of Object.entries(headers)) {
19
+ if (name.toLowerCase() === exports.SESSION_TYPE_HEADER &&
20
+ String(value).toLowerCase() === 'stateful') {
21
+ return true;
22
+ }
23
+ }
24
+ return false;
25
+ }
@@ -49,7 +49,7 @@ The filter also applies to cookies supplied in request headers and retries.
49
49
  | --- | --- |
50
50
  | HTTP (on-prem / cloud) | Sends `x-sap-adt-sessiontype: stateful` and the context cookie, when held |
51
51
  | Legacy on-prem HTTP | Keeps the context cookie but omits the session-type header |
52
- | RFC | Uses its RFC conversation; `setSessionType()` adds no HTTP session-type header |
52
+ | RFC | Uses the persistent conversation for stateful requests; since 9.3.3 resets a kept stateless conversation between calls when supported |
53
53
 
54
54
  A caller-supplied stateful header is also recognized, including by the legacy
55
55
  HTTP transport before it removes that header. Custom `IAdtTransport`
@@ -57,6 +57,29 @@ implementations must read `request.stateful`; the connection no longer adds
57
57
  `x-sap-adt-sessiontype` itself. The cookie fix was verified on E19 and E98;
58
58
  BASIS 7.40 and ABAP Cloud were not integration-tested for this change.
59
59
 
60
+ ## RFC conversation isolation (9.3.2 and 9.3.3)
61
+
62
+ Stateful requests, including LOCK and UNLOCK, share the conversation opened
63
+ by `connect()`. Since 9.3.3, `sap-rfc-lite` 0.2.0 and newer let stateless calls
64
+ reuse a second conversation: the transport resets its ABAP server context after
65
+ each call, keeping the RFC logon open. With older clients (0.1.x), or when a
66
+ resettable conversation is busy, each stateless call gets a separate temporary
67
+ conversation. Both `setSessionType('stateful')` and a caller-supplied stateful
68
+ session header select the persistent conversation.
69
+
70
+ Previously all RFC requests shared one ABAP session. On E19, saving a package
71
+ left state in `CL_PACKAGE`'s instance buffer: later writes or deletes failed
72
+ with **400 PAK/058, “Package … is already locked”**. Reading immediately after
73
+ creation could fail with **400 SADT_RESOURCE/007**. With separate conversations,
74
+ create, read, two lock/PUT/unlock rounds and delete passed on one connection.
75
+
76
+ When using `sap-rfc-lite` 0.1.x, each stateless call pays for an RFC logon:
77
+ about 0.5 s on E19, in addition to about 0.1 s for the call. With 0.2.0+, a
78
+ resettable call took about 0.24 s total on E19. Direct connector users must mark
79
+ LOCK and UNLOCK stateful to retain their conversation. If disconnect occurs
80
+ while a temporary conversation is logging on, it is closed without sending
81
+ the request; reconnect does not revive that pending request.
82
+
60
83
  ## Enabling Stateful Sessions
61
84
 
62
85
  Every example below builds on this much, so it is stated once:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/connection",
3
- "version": "9.3.1",
3
+ "version": "9.3.3",
4
4
  "description": "ABAP connection layer for MCP ABAP ADT server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -63,7 +63,7 @@
63
63
  "open": "^11.0.0"
64
64
  },
65
65
  "optionalDependencies": {
66
- "@mcp-abap-adt/sap-rfc-lite": "^0.1.0"
66
+ "@mcp-abap-adt/sap-rfc-lite": "^0.1.0 || ^0.2.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@biomejs/biome": "^2.3.14",