@abapify/adt-locks 0.4.1 → 0.4.4

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/README.md CHANGED
@@ -52,6 +52,21 @@ const acquired = await session.acquireAll();
52
52
  await session.releaseAll();
53
53
  ```
54
54
 
55
+ ## Recovery after a crash
56
+
57
+ SAP ADT requires the exact `lockHandle` returned by `LOCK` for `UNLOCK`.
58
+ `FileLockStore` persists that handle so a later process can release it:
59
+
60
+ ```ts
61
+ await locks.forceUnlock('/sap/bc/adt/oo/classes/zcl_test');
62
+ ```
63
+
64
+ Despite its historical name, `forceUnlock()` is **not** a server-side force
65
+ operation: it sends only `UNLOCK` with a persisted handle and never attempts a
66
+ second `LOCK`. If the handle was never persisted (for example, a server-side
67
+ CTS lock created by an interrupted object `POST`), SAP cannot disclose it by
68
+ URI. The caller must wait for expiry or ask the SAP administrator to clear it.
69
+
55
70
  ## Role in the monorepo
56
71
 
57
72
  - Single source of truth for ADT locks. `@abapify/adk` (save flow),
package/dist/index.d.mts CHANGED
@@ -96,6 +96,23 @@ interface UnlockOptions {
96
96
  /** Lock handle — REQUIRED. SAP always requires an explicit handle for unlock. */
97
97
  lockHandle: string;
98
98
  }
99
+ /**
100
+ * Resolve the CTS correlation for a write performed under a SAP editor lock.
101
+ * SAP's LOCK response is authoritative because it may normalize a child task
102
+ * to the request that actually owns the object lock.
103
+ */
104
+ declare function resolveLockCorrelation(handle: Pick<LockHandle, 'correlationNumber'>, requestedTransport?: string): string | undefined;
105
+ /**
106
+ * SAP does not expose an ADT operation that returns the handle of an
107
+ * existing CTS/editor lock. A caller can only unlock a handle it persisted
108
+ * when it acquired that lock.
109
+ */
110
+ declare class AdtLockHandleUnavailableError extends Error {
111
+ readonly objectUri: string;
112
+ readonly code: "ADT_LOCK_HANDLE_UNAVAILABLE";
113
+ readonly name = "AdtLockHandleUnavailableError";
114
+ constructor(objectUri: string);
115
+ }
99
116
  /**
100
117
  * Parse SAP lock response XML to extract lock handle and correlation info.
101
118
  *
@@ -115,12 +132,11 @@ interface LockService {
115
132
  /** Release a lock on an ADT object (lock handle required) */
116
133
  unlock(objectUri: string, options: UnlockOptions): Promise<void>;
117
134
  /**
118
- * Force-unlock: acquire a lock to recover the handle, then immediately unlock.
119
- * Works when the object is locked by the current user but the handle was lost
120
- * (e.g., crashed session). If the object is locked by another user, the lock
121
- * call will fail with 403/409.
135
+ * Unlock an object using the handle persisted when this client previously
136
+ * acquired the lock. The historical name is retained for compatibility;
137
+ * SAP has no server-side force-unlock operation and this never issues LOCK.
122
138
  */
123
- forceUnlock(objectUri: string, options?: LockOptions): Promise<void>;
139
+ forceUnlock(objectUri: string): Promise<void>;
124
140
  /** List all persisted lock entries (from store) */
125
141
  list(): LockEntry[];
126
142
  /** Try to UNLOCK every persisted entry, removing successful ones from store */
@@ -179,5 +195,5 @@ interface BatchLockSession {
179
195
  */
180
196
  declare function createBatchLockSession(lockService: Pick<LockService, 'lock' | 'unlock'>, targets: BatchLockTarget[], options?: BatchLockSessionOptions): BatchLockSession;
181
197
  //#endregion
182
- export { type BatchLockAcquisition, type BatchLockSession, type BatchLockSessionOptions, type BatchLockTarget, type BatchReleaseResult, FileLockStore, type LockClient, type LockEntry, type LockHandle, type LockOptions, type LockService, type LockStore, type UnlockOptions, createBatchLockSession, createLockService, parseLockResponse };
198
+ export { AdtLockHandleUnavailableError, type BatchLockAcquisition, type BatchLockSession, type BatchLockSessionOptions, type BatchLockTarget, type BatchReleaseResult, FileLockStore, type LockClient, type LockEntry, type LockHandle, type LockOptions, type LockService, type LockStore, type UnlockOptions, createBatchLockSession, createLockService, parseLockResponse, resolveLockCorrelation };
183
199
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/store.ts","../src/service.ts","../src/batch/batch-lock-session.ts"],"mappings":";;AAWA;;;;;;;;;UAAiB,UAAA;EAYS;EAVxB,MAAA;EAUwB;EARxB,iBAAA;EAYA;EAVA,eAAA;AAAA;;;;UAMe,SAAA;EAcP;EAZR,SAAA;;EAEA,UAAA;ECVe;EDYf,UAAA;;EAEA,UAAA;ECZA;EDcA,SAAA;ECdS;EDgBT,IAAA;ECdW;EDgBX,QAAA;AAAA;;;;;;AAdF;;UCNiB,SAAA;EDMS;ECJxB,QAAA,CAAS,KAAA,EAAO,SAAA;EDQhB;ECNA,UAAA,CAAW,SAAA;EDUX;ECRA,IAAA,IAAQ,SAAA;EDYR;ECVA,KAAA;AAAA;;;;;AARF;;;cAkBa,aAAA,YAAyB,SAAA;EAAA,iBACnB,QAAA;cAEL,OAAA;EAKZ,QAAA,CAAS,KAAA,EAAO,SAAA;EAQhB,UAAA,CAAW,SAAA;EAQX,IAAA,CAAA,GAAQ,SAAA;EAIR,KAAA,CAAA;EAAA,QAIQ,IAAA;EAAA,QASA,IAAA;AAAA;;;;UC1DO,UAAA;EACf,KAAA,CACE,GAAA,UACA,OAAA;IACE,MAAA;IACA,OAAA,GAAU,MAAA;IACV,IAAA;EAAA,IAED,OAAA;AAAA;AAAA,UAGY,kBAAA;;EAEf,KAAA,GAAQ,SAAA;AAAA;AAAA,UAGO,WAAA;;EAEf,SAAA;EDjBA;ECmBA,UAAA;EDnBS;ECqBT,UAAA;AAAA;AAAA,UAGe,aAAA;EDpBP;ECsBR,UAAA;AAAA;;ADVF;;;;;;;;;;;iBCyBgB,iBAAA,CAAkB,GAAA,WAAc,UAAA;AAAA,UAiB/B,WAAA;EDlCC;ECoChB,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAA,GAAc,OAAA,CAAQ,UAAA;ED5BxD;EC8BA,MAAA,CAAO,SAAA,UAAmB,OAAA,EAAS,aAAA,GAAgB,OAAA;EDtBnD;;;;;;EC6BA,WAAA,CAAY,SAAA,UAAmB,OAAA,GAAU,WAAA,GAAc,OAAA;;EAEvD,IAAA,IAAQ,SAAA;;EAER,OAAA,IAAW,OAAA;IAAU,EAAA;IAAY,MAAA;EAAA;EAzEjC;EA2EA,KAAA;AAAA;AAAA,iBAGc,iBAAA,CACd,MAAA,EAAQ,UAAA,EACR,cAAA,GAAiB,kBAAA,GAChB,WAAA;;;UCrEc,eAAA;EFZC;EEchB,SAAA;EFZA;EEcA,OAAA,GAAU,WAAA;AAAA;AAAA,UAGK,oBAAA;EACf,MAAA,EAAQ,eAAA;EACR,MAAA,EAAQ,UAAA;AAAA;AAAA,UAGO,kBAAA;EACf,QAAA;EACA,MAAA,EAAQ,KAAA;IAAQ,SAAA;IAAmB,KAAA;EAAA;AAAA;AAAA,UAGpB,uBAAA;EFbqB;;;;;;EEoBpC,eAAA;AAAA;AAAA,UAGe,gBAAA;EFCf;EECA,KAAA,IAAS,OAAA,CAAQ,oBAAA;EFGjB;EEDA,OAAA,IAAW,OAAA,CAAQ,kBAAA;EFcX;EEZR,OAAA,IAAW,aAAA,CAAc,oBAAA;EFYb;EAAA,SEVH,MAAA;AAAA;;ADhDX;;;;;;iBC0DgB,sBAAA,CACd,WAAA,EAAa,IAAA,CAAK,WAAA,sBAClB,OAAA,EAAS,eAAA,IACT,OAAA,GAAU,uBAAA,GACT,gBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/store.ts","../src/service.ts","../src/batch/batch-lock-session.ts"],"mappings":";;;;;;;;;;;UAWiB;;EAEf;;EAEA;;EAEA;;;;;UAMe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;UCpBe;;EAEf,SAAS,OAAO;;EAEhB,WAAW;;EAEX,QAAQ;;EAER;;;;;;;;;cAUW,yBAAyB;mBACnB;EAEL,YAAA;EAKZ,SAAS,OAAO;EAQhB,WAAW;EAQX,QAAQ;EAIR;UAIQ;UASA;;;;;UC1DO;EACf,MACE,aACA;IACE;IACA,UAAU;IACV;MAED;;UAGY;;EAEf,QAAQ;;UAGO;;EAEf;;EAEA;;EAEA;;UAGe;;EAEf;;;;;;;iBAQc,uBACd,QAAQ,KAAK,kCACb;;;;;;cAUW,sCAAsC;WAI5B;WAHZ;WACS;EAEG,YAAA;;;;;;;;;;;;;;iBAoBP,kBAAkB,cAAc;UAiB/B;;EAEf,KAAK,mBAAmB,UAAU,cAAc,QAAQ;;EAExD,OAAO,mBAAmB,SAAS,gBAAgB;;;;;;EAMnD,YAAY,oBAAoB;;EAEhC,QAAQ;;EAER,WAAW;IAAU;IAAY;;;EAEjC;;iBAGc,kBACd,QAAQ,YACR,iBAAiB,qBAChB;;;UCjGc;;EAEf;;EAEA,UAAU;;UAGK;EACf,QAAQ;EACR,QAAQ;;UAGO;EACf;EACA,QAAQ;IAAQ;IAAmB;;;UAGpB;;;;;;;EAOf;;UAGe;;EAEf,SAAS,QAAQ;;EAEjB,WAAW,QAAQ;;EAEnB,WAAW,cAAc;;WAEhB;;;;;;;;;iBAUK,uBACd,aAAa,KAAK,iCAClB,SAAS,mBACT,UAAU,0BACT"}
package/dist/index.mjs CHANGED
@@ -19,7 +19,8 @@ import { homedir } from "node:os";
19
19
  var FileLockStore = class {
20
20
  filePath;
21
21
  constructor(baseDir) {
22
- this.filePath = join(baseDir ?? join(homedir(), ".adt"), "locks.json");
22
+ const dir = baseDir ?? join(homedir(), ".adt");
23
+ this.filePath = join(dir, "locks.json");
23
24
  }
24
25
  register(entry) {
25
26
  const entries = this.load();
@@ -57,6 +58,28 @@ var FileLockStore = class {
57
58
  //#endregion
58
59
  //#region src/service.ts
59
60
  /**
61
+ * Resolve the CTS correlation for a write performed under a SAP editor lock.
62
+ * SAP's LOCK response is authoritative because it may normalize a child task
63
+ * to the request that actually owns the object lock.
64
+ */
65
+ function resolveLockCorrelation(handle, requestedTransport) {
66
+ return handle.correlationNumber || requestedTransport;
67
+ }
68
+ /**
69
+ * SAP does not expose an ADT operation that returns the handle of an
70
+ * existing CTS/editor lock. A caller can only unlock a handle it persisted
71
+ * when it acquired that lock.
72
+ */
73
+ var AdtLockHandleUnavailableError = class extends Error {
74
+ objectUri;
75
+ code = "ADT_LOCK_HANDLE_UNAVAILABLE";
76
+ name = "AdtLockHandleUnavailableError";
77
+ constructor(objectUri) {
78
+ super(`No persisted lock handle is available for ${objectUri}. SAP cannot force-unlock this object by URI; wait for the server lock to expire or ask the SAP administrator to clear it.`);
79
+ this.objectUri = objectUri;
80
+ }
81
+ };
82
+ /**
60
83
  * Parse SAP lock response XML to extract lock handle and correlation info.
61
84
  *
62
85
  * Response format:
@@ -118,9 +141,10 @@ function createLockService(client, serviceOptions) {
118
141
  });
119
142
  store?.deregister(objectUri);
120
143
  },
121
- async forceUnlock(objectUri, options) {
122
- const handle = await this.lock(objectUri, options);
123
- await this.unlock(objectUri, { lockHandle: handle.handle });
144
+ async forceUnlock(objectUri) {
145
+ const entry = store?.list()?.find((item) => item.objectUri === objectUri);
146
+ if (!entry) throw new AdtLockHandleUnavailableError(objectUri);
147
+ await this.unlock(objectUri, { lockHandle: entry.lockHandle });
124
148
  },
125
149
  list() {
126
150
  return store?.list() ?? [];
@@ -222,5 +246,5 @@ function createBatchLockSession(lockService, targets, options) {
222
246
  }
223
247
 
224
248
  //#endregion
225
- export { FileLockStore, createBatchLockSession, createLockService, parseLockResponse };
249
+ export { AdtLockHandleUnavailableError, FileLockStore, createBatchLockSession, createLockService, parseLockResponse, resolveLockCorrelation };
226
250
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/store.ts","../src/service.ts","../src/batch/batch-lock-session.ts"],"sourcesContent":["/**\n * Lock Store — persistence layer for ADT lock handles\n *\n * Provides an interface for registering/deregistering lock entries\n * and a file-based implementation that persists to `~/.adt/locks.json`.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport type { LockEntry } from './types';\n\n/**\n * Lock store interface — opt-in persistence for ADT lock handles.\n *\n * Implementations can be file-based, in-memory, or backed by any storage.\n */\nexport interface LockStore {\n /** Persist a lock entry after successful LOCK */\n register(entry: LockEntry): void;\n /** Remove a lock entry after successful UNLOCK */\n deregister(objectUri: string): void;\n /** List all persisted lock entries */\n list(): LockEntry[];\n /** Remove all entries */\n clear(): void;\n}\n\n/**\n * File-based lock store.\n *\n * Persists lock entries to a JSON file (default: `~/.adt/locks.json`).\n * Each entry is keyed by objectUri — re-locking the same URI overwrites.\n * File permissions are 0o600 (owner read/write only).\n */\nexport class FileLockStore implements LockStore {\n private readonly filePath: string;\n\n constructor(baseDir?: string) {\n const dir = baseDir ?? join(homedir(), '.adt');\n this.filePath = join(dir, 'locks.json');\n }\n\n register(entry: LockEntry): void {\n const entries = this.load();\n const idx = entries.findIndex((e) => e.objectUri === entry.objectUri);\n if (idx >= 0) entries[idx] = entry;\n else entries.push(entry);\n this.save(entries);\n }\n\n deregister(objectUri: string): void {\n const entries = this.load();\n const filtered = entries.filter((e) => e.objectUri !== objectUri);\n if (filtered.length !== entries.length) {\n this.save(filtered);\n }\n }\n\n list(): LockEntry[] {\n return this.load();\n }\n\n clear(): void {\n this.save([]);\n }\n\n private load(): LockEntry[] {\n if (!existsSync(this.filePath)) return [];\n try {\n return JSON.parse(readFileSync(this.filePath, 'utf8')) as LockEntry[];\n } catch {\n return [];\n }\n }\n\n private save(entries: LockEntry[]): void {\n const dir = dirname(this.filePath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n writeFileSync(this.filePath, JSON.stringify(entries, null, 2), {\n mode: 0o600,\n });\n }\n}\n","/**\n * Lock Service — lock/unlock ADT objects via REST API\n *\n * Provides standalone lock/unlock operations that work with any\n * object URI. Optionally integrates with a LockStore for persistence.\n *\n * Usage:\n * import { createLockService, FileLockStore } from '@abapify/adt-locks';\n *\n * const locks = createLockService(client, { store: new FileLockStore() });\n * const handle = await locks.lock('/sap/bc/adt/oo/classes/zcl_test');\n * await locks.unlock('/sap/bc/adt/oo/classes/zcl_test', handle);\n */\n\nimport type { LockHandle, LockEntry } from './types';\nimport type { LockStore } from './store';\n\n/** Minimal client interface — anything with an authenticated fetch() */\nexport interface LockClient {\n fetch(\n url: string,\n options?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n ): Promise<unknown>;\n}\n\nexport interface LockServiceOptions {\n /** Optional lock store for persistence */\n store?: LockStore;\n}\n\nexport interface LockOptions {\n /** Transport request */\n transport?: string;\n /** Object name (for store metadata) */\n objectName?: string;\n /** Object type (for store metadata) */\n objectType?: string;\n}\n\nexport interface UnlockOptions {\n /** Lock handle — REQUIRED. SAP always requires an explicit handle for unlock. */\n lockHandle: string;\n}\n\n/**\n * Parse SAP lock response XML to extract lock handle and correlation info.\n *\n * Response format:\n * ```xml\n * <asx:abap>...<DATA>\n * <LOCK_HANDLE>xxx</LOCK_HANDLE>\n * <CORRNR>yyy</CORRNR>\n * <CORRUSER>zzz</CORRUSER>\n * </DATA>...</asx:abap>\n * ```\n */\nexport function parseLockResponse(xml: string): LockHandle {\n const handleMatch = xml.match(/<LOCK_HANDLE>([^<]+)<\\/LOCK_HANDLE>/);\n if (!handleMatch) {\n throw new Error(\n 'Failed to parse lock handle from SAP response. The response format may have changed.',\n );\n }\n const corrNrMatch = xml.match(/<CORRNR>([^<]+)<\\/CORRNR>/);\n const corrUserMatch = xml.match(/<CORRUSER>([^<]+)<\\/CORRUSER>/);\n\n return {\n handle: handleMatch[1],\n correlationNumber: corrNrMatch?.[1],\n correlationUser: corrUserMatch?.[1],\n };\n}\n\nexport interface LockService {\n /** Acquire a lock on an ADT object */\n lock(objectUri: string, options?: LockOptions): Promise<LockHandle>;\n /** Release a lock on an ADT object (lock handle required) */\n unlock(objectUri: string, options: UnlockOptions): Promise<void>;\n /**\n * Force-unlock: acquire a lock to recover the handle, then immediately unlock.\n * Works when the object is locked by the current user but the handle was lost\n * (e.g., crashed session). If the object is locked by another user, the lock\n * call will fail with 403/409.\n */\n forceUnlock(objectUri: string, options?: LockOptions): Promise<void>;\n /** List all persisted lock entries (from store) */\n list(): LockEntry[];\n /** Try to UNLOCK every persisted entry, removing successful ones from store */\n cleanup(): Promise<{ ok: number; failed: number }>;\n /** Clear the store without sending any UNLOCK requests */\n clear(): void;\n}\n\nexport function createLockService(\n client: LockClient,\n serviceOptions?: LockServiceOptions,\n): LockService {\n const store = serviceOptions?.store;\n\n /** Headers for lock operations — stateful session with security session reuse */\n const lockHeaders = {\n 'X-sap-adt-sessiontype': 'stateful',\n 'x-sap-security-session': 'use',\n Accept:\n 'application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result',\n };\n\n /** Headers for unlock operations */\n const unlockHeaders = {\n 'X-sap-adt-sessiontype': 'stateful',\n 'x-sap-security-session': 'use',\n };\n\n return {\n async lock(objectUri, options) {\n const query = options?.transport\n ? `?_action=LOCK&accessMode=MODIFY&corrNr=${options.transport}`\n : '?_action=LOCK&accessMode=MODIFY';\n\n const response = await client.fetch(`${objectUri}${query}`, {\n method: 'POST',\n headers: lockHeaders,\n });\n\n // The adapter may return a string (text/xml) or a parsed object;\n // parseLockResponse needs the raw XML string.\n const body =\n typeof response === 'string'\n ? response\n : (response as Response)?.text\n ? await (response as Response).text()\n : JSON.stringify(response);\n const handle = parseLockResponse(body);\n\n store?.register({\n objectUri,\n objectName:\n options?.objectName ?? objectUri.split('/').pop() ?? objectUri,\n objectType: options?.objectType,\n lockHandle: handle.handle,\n transport: handle.correlationNumber,\n lockedAt: new Date().toISOString(),\n });\n\n return handle;\n },\n\n async unlock(objectUri, options) {\n const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(options.lockHandle)}`;\n\n await client.fetch(`${objectUri}${query}`, {\n method: 'POST',\n headers: unlockHeaders,\n });\n\n store?.deregister(objectUri);\n },\n\n async forceUnlock(objectUri, options) {\n // Lock to recover/acquire the handle, then immediately unlock\n const handle = await this.lock(objectUri, options);\n await this.unlock(objectUri, { lockHandle: handle.handle });\n },\n\n list() {\n return store?.list() ?? [];\n },\n\n async cleanup() {\n const entries = store?.list() ?? [];\n let ok = 0;\n let failed = 0;\n\n for (const entry of entries) {\n try {\n const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(entry.lockHandle)}`;\n await client.fetch(`${entry.objectUri}${query}`, {\n method: 'POST',\n headers: unlockHeaders,\n });\n store?.deregister(entry.objectUri);\n ok++;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('not locked') || msg.includes('not enqueued')) {\n store?.deregister(entry.objectUri);\n ok++;\n } else {\n failed++;\n }\n }\n }\n\n return { ok, failed };\n },\n\n clear() {\n store?.clear();\n },\n };\n}\n","/**\n * BatchLockSession — acquire and release N locks with best-effort rollback.\n *\n * SAP ADT has no transactional multi-object save. A \"batch lock\" is therefore\n * a **best-effort** construct:\n *\n * - `begin()` sequentially acquires a lock on every target. If any lock\n * acquisition fails, every previously acquired lock in this session is\n * released before the error is rethrown — leaving the backend in the\n * same state it was before `begin()` was called.\n *\n * - `release()` unlocks every successfully acquired handle, swallowing\n * per-object unlock failures (they are surfaced through the returned\n * result for the caller to inspect).\n *\n * - `handles()` exposes the acquired handles so callers can thread them\n * into downstream save/PUT flows without re-locking.\n *\n * The session is **single-use**: once released (explicitly or via rollback),\n * it cannot be reused. Tests exercise acquire/release/partial-failure paths.\n *\n * Consumer rationale: ADK's per-object `save({mode:'update'})` already owns\n * its own lock/unlock cycle. `BatchLockSession` is therefore positioned as an\n * outer pre-flight primitive — acquire across a whole ChangePlan to catch\n * conflicts BEFORE we start mutating SAP, release before handing off to ADK.\n * When ADK's save cycle later re-locks per object it simply re-uses the same\n * security session (the CSRF token survives), so the double lock is cheap.\n */\nimport type { LockHandle } from '../types';\nimport type { LockService, LockOptions } from '../service';\n\nexport interface BatchLockTarget {\n /** ADT object URI — e.g. `/sap/bc/adt/oo/classes/zcl_foo` */\n objectUri: string;\n /** Per-target lock options (transport, object name/type for the store). */\n options?: LockOptions;\n}\n\nexport interface BatchLockAcquisition {\n target: BatchLockTarget;\n handle: LockHandle;\n}\n\nexport interface BatchReleaseResult {\n released: number;\n failed: Array<{ objectUri: string; error: string }>;\n}\n\nexport interface BatchLockSessionOptions {\n /**\n * When true, `begin()` stops on the first failure and rolls back locks\n * already acquired in this session. When false (rare), errors are\n * collected and returned but already-acquired locks are kept — use only\n * for diagnostic tooling. Default: true.\n */\n rollbackOnError?: boolean;\n}\n\nexport interface BatchLockSession {\n /** Acquire all configured locks. Rollback on failure (by default). */\n begin(): Promise<BatchLockAcquisition[]>;\n /** Release every currently held handle. Safe to call multiple times. */\n release(): Promise<BatchReleaseResult>;\n /** Snapshot of currently held handles. */\n handles(): ReadonlyArray<BatchLockAcquisition>;\n /** Has `begin()` completed successfully? */\n readonly active: boolean;\n}\n\n/**\n * Create a batch lock session over a set of targets.\n *\n * The session does NOT auto-begin; callers drive the lifecycle explicitly so\n * error handling stays visible at the call-site (`try { begin(); … } finally\n * { release(); }`).\n */\nexport function createBatchLockSession(\n lockService: Pick<LockService, 'lock' | 'unlock'>,\n targets: BatchLockTarget[],\n options?: BatchLockSessionOptions,\n): BatchLockSession {\n const rollback = options?.rollbackOnError ?? true;\n const acquired: BatchLockAcquisition[] = [];\n let beganOnce = false;\n let released = false;\n\n return {\n async begin() {\n if (beganOnce) {\n throw new Error('BatchLockSession.begin() called more than once');\n }\n beganOnce = true;\n\n for (const target of targets) {\n try {\n const handle = await lockService.lock(\n target.objectUri,\n target.options,\n );\n acquired.push({ target, handle });\n } catch (err) {\n if (rollback) {\n // Best-effort: release what we already have, then propagate.\n for (const a of acquired.splice(0).reverse()) {\n try {\n await lockService.unlock(a.target.objectUri, {\n lockHandle: a.handle.handle,\n });\n } catch {\n /* swallow — primary failure is the real signal */\n }\n }\n // Session is done — no further release() work possible.\n released = true;\n }\n throw err instanceof Error\n ? err\n : new Error(`Batch lock failed: ${String(err)}`);\n }\n }\n return [...acquired];\n },\n\n async release() {\n if (released) return { released: 0, failed: [] };\n released = true;\n\n const failed: BatchReleaseResult['failed'] = [];\n let ok = 0;\n // Release in reverse acquisition order — symmetric with begin()'s\n // rollback path and avoids surprising the SAP enqueue server.\n for (const a of acquired.splice(0).reverse()) {\n try {\n await lockService.unlock(a.target.objectUri, {\n lockHandle: a.handle.handle,\n });\n ok++;\n } catch (err) {\n failed.push({\n objectUri: a.target.objectUri,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n return { released: ok, failed };\n },\n\n handles() {\n return [...acquired];\n },\n\n get active() {\n return beganOnce && !released;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/store.ts","../src/service.ts","../src/batch/batch-lock-session.ts"],"sourcesContent":["/**\n * Lock Store — persistence layer for ADT lock handles\n *\n * Provides an interface for registering/deregistering lock entries\n * and a file-based implementation that persists to `~/.adt/locks.json`.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport type { LockEntry } from './types';\n\n/**\n * Lock store interface — opt-in persistence for ADT lock handles.\n *\n * Implementations can be file-based, in-memory, or backed by any storage.\n */\nexport interface LockStore {\n /** Persist a lock entry after successful LOCK */\n register(entry: LockEntry): void;\n /** Remove a lock entry after successful UNLOCK */\n deregister(objectUri: string): void;\n /** List all persisted lock entries */\n list(): LockEntry[];\n /** Remove all entries */\n clear(): void;\n}\n\n/**\n * File-based lock store.\n *\n * Persists lock entries to a JSON file (default: `~/.adt/locks.json`).\n * Each entry is keyed by objectUri — re-locking the same URI overwrites.\n * File permissions are 0o600 (owner read/write only).\n */\nexport class FileLockStore implements LockStore {\n private readonly filePath: string;\n\n constructor(baseDir?: string) {\n const dir = baseDir ?? join(homedir(), '.adt');\n this.filePath = join(dir, 'locks.json');\n }\n\n register(entry: LockEntry): void {\n const entries = this.load();\n const idx = entries.findIndex((e) => e.objectUri === entry.objectUri);\n if (idx >= 0) entries[idx] = entry;\n else entries.push(entry);\n this.save(entries);\n }\n\n deregister(objectUri: string): void {\n const entries = this.load();\n const filtered = entries.filter((e) => e.objectUri !== objectUri);\n if (filtered.length !== entries.length) {\n this.save(filtered);\n }\n }\n\n list(): LockEntry[] {\n return this.load();\n }\n\n clear(): void {\n this.save([]);\n }\n\n private load(): LockEntry[] {\n if (!existsSync(this.filePath)) return [];\n try {\n return JSON.parse(readFileSync(this.filePath, 'utf8')) as LockEntry[];\n } catch {\n return [];\n }\n }\n\n private save(entries: LockEntry[]): void {\n const dir = dirname(this.filePath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n writeFileSync(this.filePath, JSON.stringify(entries, null, 2), {\n mode: 0o600,\n });\n }\n}\n","/**\n * Lock Service — lock/unlock ADT objects via REST API\n *\n * Provides standalone lock/unlock operations that work with any\n * object URI. Optionally integrates with a LockStore for persistence.\n *\n * Usage:\n * import { createLockService, FileLockStore } from '@abapify/adt-locks';\n *\n * const locks = createLockService(client, { store: new FileLockStore() });\n * const handle = await locks.lock('/sap/bc/adt/oo/classes/zcl_test');\n * await locks.unlock('/sap/bc/adt/oo/classes/zcl_test', handle);\n */\n\nimport type { LockHandle, LockEntry } from './types';\nimport type { LockStore } from './store';\n\n/** Minimal client interface — anything with an authenticated fetch() */\nexport interface LockClient {\n fetch(\n url: string,\n options?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n ): Promise<unknown>;\n}\n\nexport interface LockServiceOptions {\n /** Optional lock store for persistence */\n store?: LockStore;\n}\n\nexport interface LockOptions {\n /** Transport request */\n transport?: string;\n /** Object name (for store metadata) */\n objectName?: string;\n /** Object type (for store metadata) */\n objectType?: string;\n}\n\nexport interface UnlockOptions {\n /** Lock handle — REQUIRED. SAP always requires an explicit handle for unlock. */\n lockHandle: string;\n}\n\n/**\n * Resolve the CTS correlation for a write performed under a SAP editor lock.\n * SAP's LOCK response is authoritative because it may normalize a child task\n * to the request that actually owns the object lock.\n */\nexport function resolveLockCorrelation(\n handle: Pick<LockHandle, 'correlationNumber'>,\n requestedTransport?: string,\n): string | undefined {\n return handle.correlationNumber || requestedTransport;\n}\n\n/**\n * SAP does not expose an ADT operation that returns the handle of an\n * existing CTS/editor lock. A caller can only unlock a handle it persisted\n * when it acquired that lock.\n */\nexport class AdtLockHandleUnavailableError extends Error {\n readonly code = 'ADT_LOCK_HANDLE_UNAVAILABLE' as const;\n override readonly name = 'AdtLockHandleUnavailableError';\n\n constructor(readonly objectUri: string) {\n super(\n `No persisted lock handle is available for ${objectUri}. ` +\n 'SAP cannot force-unlock this object by URI; wait for the server lock to expire or ask the SAP administrator to clear it.',\n );\n }\n}\n\n/**\n * Parse SAP lock response XML to extract lock handle and correlation info.\n *\n * Response format:\n * ```xml\n * <asx:abap>...<DATA>\n * <LOCK_HANDLE>xxx</LOCK_HANDLE>\n * <CORRNR>yyy</CORRNR>\n * <CORRUSER>zzz</CORRUSER>\n * </DATA>...</asx:abap>\n * ```\n */\nexport function parseLockResponse(xml: string): LockHandle {\n const handleMatch = xml.match(/<LOCK_HANDLE>([^<]+)<\\/LOCK_HANDLE>/);\n if (!handleMatch) {\n throw new Error(\n 'Failed to parse lock handle from SAP response. The response format may have changed.',\n );\n }\n const corrNrMatch = xml.match(/<CORRNR>([^<]+)<\\/CORRNR>/);\n const corrUserMatch = xml.match(/<CORRUSER>([^<]+)<\\/CORRUSER>/);\n\n return {\n handle: handleMatch[1],\n correlationNumber: corrNrMatch?.[1],\n correlationUser: corrUserMatch?.[1],\n };\n}\n\nexport interface LockService {\n /** Acquire a lock on an ADT object */\n lock(objectUri: string, options?: LockOptions): Promise<LockHandle>;\n /** Release a lock on an ADT object (lock handle required) */\n unlock(objectUri: string, options: UnlockOptions): Promise<void>;\n /**\n * Unlock an object using the handle persisted when this client previously\n * acquired the lock. The historical name is retained for compatibility;\n * SAP has no server-side force-unlock operation and this never issues LOCK.\n */\n forceUnlock(objectUri: string): Promise<void>;\n /** List all persisted lock entries (from store) */\n list(): LockEntry[];\n /** Try to UNLOCK every persisted entry, removing successful ones from store */\n cleanup(): Promise<{ ok: number; failed: number }>;\n /** Clear the store without sending any UNLOCK requests */\n clear(): void;\n}\n\nexport function createLockService(\n client: LockClient,\n serviceOptions?: LockServiceOptions,\n): LockService {\n const store = serviceOptions?.store;\n\n /** Headers for lock operations — stateful session with security session reuse */\n const lockHeaders = {\n 'X-sap-adt-sessiontype': 'stateful',\n 'x-sap-security-session': 'use',\n Accept:\n 'application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result',\n };\n\n /** Headers for unlock operations */\n const unlockHeaders = {\n 'X-sap-adt-sessiontype': 'stateful',\n 'x-sap-security-session': 'use',\n };\n\n return {\n async lock(objectUri, options) {\n const query = options?.transport\n ? `?_action=LOCK&accessMode=MODIFY&corrNr=${options.transport}`\n : '?_action=LOCK&accessMode=MODIFY';\n\n const response = await client.fetch(`${objectUri}${query}`, {\n method: 'POST',\n headers: lockHeaders,\n });\n\n // The adapter may return a string (text/xml) or a parsed object;\n // parseLockResponse needs the raw XML string.\n const body =\n typeof response === 'string'\n ? response\n : (response as Response)?.text\n ? await (response as Response).text()\n : JSON.stringify(response);\n const handle = parseLockResponse(body);\n\n store?.register({\n objectUri,\n objectName:\n options?.objectName ?? objectUri.split('/').pop() ?? objectUri,\n objectType: options?.objectType,\n lockHandle: handle.handle,\n transport: handle.correlationNumber,\n lockedAt: new Date().toISOString(),\n });\n\n return handle;\n },\n\n async unlock(objectUri, options) {\n const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(options.lockHandle)}`;\n\n await client.fetch(`${objectUri}${query}`, {\n method: 'POST',\n headers: unlockHeaders,\n });\n\n store?.deregister(objectUri);\n },\n\n async forceUnlock(objectUri) {\n // A second LOCK cannot recover a lost handle: real SAP systems such as\n // TRL reject it with an \"already locked in request\" conflict. Re-locking\n // also risks creating a new CTS lock on systems that permit it. Only a\n // handle registered by a previous successful LOCK is safe to use.\n const entry = store?.list()?.find((item) => item.objectUri === objectUri);\n if (!entry) {\n throw new AdtLockHandleUnavailableError(objectUri);\n }\n await this.unlock(objectUri, { lockHandle: entry.lockHandle });\n },\n\n list() {\n return store?.list() ?? [];\n },\n\n async cleanup() {\n const entries = store?.list() ?? [];\n let ok = 0;\n let failed = 0;\n\n for (const entry of entries) {\n try {\n const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(entry.lockHandle)}`;\n await client.fetch(`${entry.objectUri}${query}`, {\n method: 'POST',\n headers: unlockHeaders,\n });\n store?.deregister(entry.objectUri);\n ok++;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('not locked') || msg.includes('not enqueued')) {\n store?.deregister(entry.objectUri);\n ok++;\n } else {\n failed++;\n }\n }\n }\n\n return { ok, failed };\n },\n\n clear() {\n store?.clear();\n },\n };\n}\n","/**\n * BatchLockSession — acquire and release N locks with best-effort rollback.\n *\n * SAP ADT has no transactional multi-object save. A \"batch lock\" is therefore\n * a **best-effort** construct:\n *\n * - `begin()` sequentially acquires a lock on every target. If any lock\n * acquisition fails, every previously acquired lock in this session is\n * released before the error is rethrown — leaving the backend in the\n * same state it was before `begin()` was called.\n *\n * - `release()` unlocks every successfully acquired handle, swallowing\n * per-object unlock failures (they are surfaced through the returned\n * result for the caller to inspect).\n *\n * - `handles()` exposes the acquired handles so callers can thread them\n * into downstream save/PUT flows without re-locking.\n *\n * The session is **single-use**: once released (explicitly or via rollback),\n * it cannot be reused. Tests exercise acquire/release/partial-failure paths.\n *\n * Consumer rationale: ADK's per-object `save({mode:'update'})` already owns\n * its own lock/unlock cycle. `BatchLockSession` is therefore positioned as an\n * outer pre-flight primitive — acquire across a whole ChangePlan to catch\n * conflicts BEFORE we start mutating SAP, release before handing off to ADK.\n * When ADK's save cycle later re-locks per object it simply re-uses the same\n * security session (the CSRF token survives), so the double lock is cheap.\n */\nimport type { LockHandle } from '../types';\nimport type { LockService, LockOptions } from '../service';\n\nexport interface BatchLockTarget {\n /** ADT object URI — e.g. `/sap/bc/adt/oo/classes/zcl_foo` */\n objectUri: string;\n /** Per-target lock options (transport, object name/type for the store). */\n options?: LockOptions;\n}\n\nexport interface BatchLockAcquisition {\n target: BatchLockTarget;\n handle: LockHandle;\n}\n\nexport interface BatchReleaseResult {\n released: number;\n failed: Array<{ objectUri: string; error: string }>;\n}\n\nexport interface BatchLockSessionOptions {\n /**\n * When true, `begin()` stops on the first failure and rolls back locks\n * already acquired in this session. When false (rare), errors are\n * collected and returned but already-acquired locks are kept — use only\n * for diagnostic tooling. Default: true.\n */\n rollbackOnError?: boolean;\n}\n\nexport interface BatchLockSession {\n /** Acquire all configured locks. Rollback on failure (by default). */\n begin(): Promise<BatchLockAcquisition[]>;\n /** Release every currently held handle. Safe to call multiple times. */\n release(): Promise<BatchReleaseResult>;\n /** Snapshot of currently held handles. */\n handles(): ReadonlyArray<BatchLockAcquisition>;\n /** Has `begin()` completed successfully? */\n readonly active: boolean;\n}\n\n/**\n * Create a batch lock session over a set of targets.\n *\n * The session does NOT auto-begin; callers drive the lifecycle explicitly so\n * error handling stays visible at the call-site (`try { begin(); … } finally\n * { release(); }`).\n */\nexport function createBatchLockSession(\n lockService: Pick<LockService, 'lock' | 'unlock'>,\n targets: BatchLockTarget[],\n options?: BatchLockSessionOptions,\n): BatchLockSession {\n const rollback = options?.rollbackOnError ?? true;\n const acquired: BatchLockAcquisition[] = [];\n let beganOnce = false;\n let released = false;\n\n return {\n async begin() {\n if (beganOnce) {\n throw new Error('BatchLockSession.begin() called more than once');\n }\n beganOnce = true;\n\n for (const target of targets) {\n try {\n const handle = await lockService.lock(\n target.objectUri,\n target.options,\n );\n acquired.push({ target, handle });\n } catch (err) {\n if (rollback) {\n // Best-effort: release what we already have, then propagate.\n for (const a of acquired.splice(0).reverse()) {\n try {\n await lockService.unlock(a.target.objectUri, {\n lockHandle: a.handle.handle,\n });\n } catch {\n /* swallow — primary failure is the real signal */\n }\n }\n // Session is done — no further release() work possible.\n released = true;\n }\n throw err instanceof Error\n ? err\n : new Error(`Batch lock failed: ${String(err)}`);\n }\n }\n return [...acquired];\n },\n\n async release() {\n if (released) return { released: 0, failed: [] };\n released = true;\n\n const failed: BatchReleaseResult['failed'] = [];\n let ok = 0;\n // Release in reverse acquisition order — symmetric with begin()'s\n // rollback path and avoids surprising the SAP enqueue server.\n for (const a of acquired.splice(0).reverse()) {\n try {\n await lockService.unlock(a.target.objectUri, {\n lockHandle: a.handle.handle,\n });\n ok++;\n } catch (err) {\n failed.push({\n objectUri: a.target.objectUri,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n return { released: ok, failed };\n },\n\n handles() {\n return [...acquired];\n },\n\n get active() {\n return beganOnce && !released;\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abapify/adt-locks",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "ADT lock/unlock operations and lock store management",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,7 @@
10
10
  "./package.json": "./package.json"
11
11
  },
12
12
  "dependencies": {
13
- "@abapify/adt-client": "0.4.1"
13
+ "@abapify/adt-client": "0.4.4"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"