@socketsecurity/lib 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.5.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.5.0) - 2025-10-28
9
+
10
+ ### Added
11
+
12
+ - **Process locking utilities**: Added `ProcessLockManager` class providing cross-platform inter-process synchronization using file-system based locks
13
+ - Atomic lock acquisition via `mkdir()` for thread-safe operations
14
+ - Stale lock detection with automatic cleanup (default 10 seconds, aligned with npm's npx strategy)
15
+ - Exponential backoff with jitter for retry attempts
16
+ - Process exit handlers for guaranteed cleanup even on abnormal termination
17
+ - Three main APIs: `acquire()`, `release()`, and `withLock()` (recommended)
18
+ - Comprehensive test suite with `describe.sequential` for proper isolation
19
+ - Export: `@socketsecurity/lib/process-lock`
20
+
21
+ ### Changed
22
+
23
+ - **Script refactoring**: Renamed `spinner.succeed()` to `spinner.success()` for consistency
24
+ - **Script cleanup**: Removed redundant spinner cleanup in interactive-runner
25
+
8
26
  ## [2.4.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.4.0) - 2025-10-28
9
27
 
10
28
  ### Changed
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Lock acquisition options.
3
+ */
4
+ export interface ProcessLockOptions {
5
+ /**
6
+ * Maximum number of retry attempts.
7
+ * @default 3
8
+ */
9
+ retries?: number | undefined;
10
+ /**
11
+ * Base delay between retries in milliseconds.
12
+ * @default 100
13
+ */
14
+ baseDelayMs?: number | undefined;
15
+ /**
16
+ * Maximum delay between retries in milliseconds.
17
+ * @default 1000
18
+ */
19
+ maxDelayMs?: number | undefined;
20
+ /**
21
+ * Stale lock timeout in milliseconds.
22
+ * Locks older than this are considered abandoned and can be reclaimed.
23
+ * Aligned with npm's npx locking strategy (5-10 seconds).
24
+ * @default 10000 (10 seconds)
25
+ */
26
+ staleMs?: number | undefined;
27
+ }
28
+ /**
29
+ * Process lock manager with stale detection and exit cleanup.
30
+ * Provides cross-platform inter-process synchronization using file-system
31
+ * based locks.
32
+ */
33
+ declare class ProcessLockManager {
34
+ private activeLocks;
35
+ private exitHandlerRegistered;
36
+ /**
37
+ * Ensure process exit handler is registered for cleanup.
38
+ * Registers a handler that cleans up all active locks when the process exits.
39
+ */
40
+ private ensureExitHandler;
41
+ /**
42
+ * Check if a lock is stale based on mtime.
43
+ * A lock is considered stale if it's older than the specified timeout,
44
+ * indicating the holding process likely died abnormally.
45
+ *
46
+ * @param lockPath - Path to the lock directory
47
+ * @param staleMs - Stale timeout in milliseconds
48
+ * @returns True if lock exists and is stale
49
+ */
50
+ private isStale;
51
+ /**
52
+ * Acquire a lock using mkdir for atomic operation.
53
+ * Handles stale locks and includes exit cleanup.
54
+ *
55
+ * This method attempts to create a lock directory atomically. If the lock
56
+ * already exists, it checks if it's stale and removes it before retrying.
57
+ * Uses exponential backoff with jitter for retry attempts.
58
+ *
59
+ * @param lockPath - Path to the lock directory
60
+ * @param options - Lock acquisition options
61
+ * @returns Release function to unlock
62
+ * @throws Error if lock cannot be acquired after all retries
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const release = await processLock.acquire('/tmp/my-lock')
67
+ * try {
68
+ * // Critical section
69
+ * } finally {
70
+ * release()
71
+ * }
72
+ * ```
73
+ */
74
+ acquire(lockPath: string, options?: ProcessLockOptions): Promise<() => void>;
75
+ /**
76
+ * Release a lock and remove from tracking.
77
+ * Removes the lock directory and stops tracking it for exit cleanup.
78
+ *
79
+ * @param lockPath - Path to the lock directory
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * processLock.release('/tmp/my-lock')
84
+ * ```
85
+ */
86
+ release(lockPath: string): void;
87
+ /**
88
+ * Execute a function with exclusive lock protection.
89
+ * Automatically handles lock acquisition, execution, and cleanup.
90
+ *
91
+ * This is the recommended way to use process locks, as it guarantees
92
+ * cleanup even if the callback throws an error.
93
+ *
94
+ * @param lockPath - Path to the lock directory
95
+ * @param fn - Function to execute while holding the lock
96
+ * @param options - Lock acquisition options
97
+ * @returns Result of the callback function
98
+ * @throws Error from callback or lock acquisition failure
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const result = await processLock.withLock('/tmp/my-lock', async () => {
103
+ * // Critical section
104
+ * return someValue
105
+ * })
106
+ * ```
107
+ */
108
+ withLock<T>(lockPath: string, fn: () => Promise<T>, options?: ProcessLockOptions): Promise<T>;
109
+ }
110
+ // Export singleton instance.
111
+ export declare const processLock: ProcessLockManager;
112
+ export {};
@@ -0,0 +1,3 @@
1
+ /* Socket Lib - Built with esbuild */
2
+ var c=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var v=(i,e)=>{for(var r in e)c(i,r,{get:e[r],enumerable:!0})},w=(i,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of g(e))!p.call(i,t)&&t!==r&&c(i,t,{get:()=>e[t],enumerable:!(n=y(e,t))||n.enumerable});return i};var x=i=>w(c({},"__esModule",{value:!0}),i);var E={};v(E,{processLock:()=>S});module.exports=x(E);var s=require("node:fs"),a=require("./fs"),u=require("./logger"),f=require("./promises"),m=require("./signal-exit");class L{activeLocks=new Set;exitHandlerRegistered=!1;ensureExitHandler(){this.exitHandlerRegistered||((0,m.onExit)(()=>{for(const e of this.activeLocks)try{(0,s.existsSync)(e)&&(0,a.safeDeleteSync)(e,{recursive:!0})}catch{}}),this.exitHandlerRegistered=!0)}isStale(e,r){try{if(!(0,s.existsSync)(e))return!1;const n=(0,s.statSync)(e);return Date.now()-n.mtime.getTime()>r}catch{return!1}}async acquire(e,r={}){const{baseDelayMs:n=100,maxDelayMs:t=1e3,retries:l=3,staleMs:d=1e4}=r;return this.ensureExitHandler(),await(0,f.pRetry)(async()=>{try{if((0,s.existsSync)(e)&&this.isStale(e,d)){u.logger.log(`Removing stale lock: ${e}`);try{(0,a.safeDeleteSync)(e,{recursive:!0})}catch{}}return(0,s.mkdirSync)(e,{recursive:!1}),this.activeLocks.add(e),()=>this.release(e)}catch(o){throw o instanceof Error&&o.code==="EEXIST"?this.isStale(e,d)?new Error(`Stale lock detected: ${e}`):new Error(`Lock already exists: ${e}`):o}},{retries:l,baseDelayMs:n,maxDelayMs:t,jitter:!0})}release(e){try{(0,s.existsSync)(e)&&(0,a.safeDeleteSync)(e,{recursive:!0}),this.activeLocks.delete(e)}catch(r){u.logger.warn(`Failed to release lock ${e}: ${r instanceof Error?r.message:String(r)}`)}}async withLock(e,r,n){const t=await this.acquire(e,n);try{return await r()}finally{t()}}}const S=new L;0&&(module.exports={processLock});
3
+ //# sourceMappingURL=process-lock.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/process-lock.ts"],
4
+ "sourcesContent": ["/**\n * @fileoverview Process locking utilities with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system based locks.\n */\n\nimport { existsSync, mkdirSync, statSync } from 'node:fs'\n\nimport { safeDeleteSync } from './fs'\nimport { logger } from './logger'\nimport { pRetry } from './promises'\nimport { onExit } from './signal-exit'\n\n/**\n * Lock acquisition options.\n */\nexport interface ProcessLockOptions {\n /**\n * Maximum number of retry attempts.\n * @default 3\n */\n retries?: number | undefined\n\n /**\n * Base delay between retries in milliseconds.\n * @default 100\n */\n baseDelayMs?: number | undefined\n\n /**\n * Maximum delay between retries in milliseconds.\n * @default 1000\n */\n maxDelayMs?: number | undefined\n\n /**\n * Stale lock timeout in milliseconds.\n * Locks older than this are considered abandoned and can be reclaimed.\n * Aligned with npm's npx locking strategy (5-10 seconds).\n * @default 10000 (10 seconds)\n */\n staleMs?: number | undefined\n}\n\n/**\n * Process lock manager with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system\n * based locks.\n */\nclass ProcessLockManager {\n private activeLocks = new Set<string>()\n private exitHandlerRegistered = false\n\n /**\n * Ensure process exit handler is registered for cleanup.\n * Registers a handler that cleans up all active locks when the process exits.\n */\n private ensureExitHandler() {\n if (this.exitHandlerRegistered) {\n return\n }\n\n onExit(() => {\n for (const lockPath of this.activeLocks) {\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n } catch {\n // Ignore cleanup errors during exit\n }\n }\n })\n\n this.exitHandlerRegistered = true\n }\n\n /**\n * Check if a lock is stale based on mtime.\n * A lock is considered stale if it's older than the specified timeout,\n * indicating the holding process likely died abnormally.\n *\n * @param lockPath - Path to the lock directory\n * @param staleMs - Stale timeout in milliseconds\n * @returns True if lock exists and is stale\n */\n private isStale(lockPath: string, staleMs: number): boolean {\n try {\n if (!existsSync(lockPath)) {\n return false\n }\n\n const stats = statSync(lockPath)\n const age = Date.now() - stats.mtime.getTime()\n return age > staleMs\n } catch {\n return false\n }\n }\n\n /**\n * Acquire a lock using mkdir for atomic operation.\n * Handles stale locks and includes exit cleanup.\n *\n * This method attempts to create a lock directory atomically. If the lock\n * already exists, it checks if it's stale and removes it before retrying.\n * Uses exponential backoff with jitter for retry attempts.\n *\n * @param lockPath - Path to the lock directory\n * @param options - Lock acquisition options\n * @returns Release function to unlock\n * @throws Error if lock cannot be acquired after all retries\n *\n * @example\n * ```typescript\n * const release = await processLock.acquire('/tmp/my-lock')\n * try {\n * // Critical section\n * } finally {\n * release()\n * }\n * ```\n */\n async acquire(\n lockPath: string,\n options: ProcessLockOptions = {},\n ): Promise<() => void> {\n const {\n baseDelayMs = 100,\n maxDelayMs = 1000,\n retries = 3,\n staleMs = 10_000,\n } = options\n\n // Ensure exit handler is registered before any lock acquisition\n this.ensureExitHandler()\n\n return await pRetry(\n async () => {\n try {\n // Check for stale lock and remove if necessary\n if (existsSync(lockPath) && this.isStale(lockPath, staleMs)) {\n logger.log(`Removing stale lock: ${lockPath}`)\n try {\n safeDeleteSync(lockPath, { recursive: true })\n } catch {\n // Ignore errors removing stale lock - will retry\n }\n }\n\n // Atomic lock acquisition via mkdir\n mkdirSync(lockPath, { recursive: false })\n\n // Track lock for cleanup\n this.activeLocks.add(lockPath)\n\n // Return release function\n return () => this.release(lockPath)\n } catch (error) {\n // Handle lock contention\n if (error instanceof Error && (error as any).code === 'EEXIST') {\n if (this.isStale(lockPath, staleMs)) {\n throw new Error(`Stale lock detected: ${lockPath}`)\n }\n throw new Error(`Lock already exists: ${lockPath}`)\n }\n throw error\n }\n },\n {\n retries,\n baseDelayMs,\n maxDelayMs,\n jitter: true,\n },\n )\n }\n\n /**\n * Release a lock and remove from tracking.\n * Removes the lock directory and stops tracking it for exit cleanup.\n *\n * @param lockPath - Path to the lock directory\n *\n * @example\n * ```typescript\n * processLock.release('/tmp/my-lock')\n * ```\n */\n release(lockPath: string): void {\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n this.activeLocks.delete(lockPath)\n } catch (error) {\n logger.warn(\n `Failed to release lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n /**\n * Execute a function with exclusive lock protection.\n * Automatically handles lock acquisition, execution, and cleanup.\n *\n * This is the recommended way to use process locks, as it guarantees\n * cleanup even if the callback throws an error.\n *\n * @param lockPath - Path to the lock directory\n * @param fn - Function to execute while holding the lock\n * @param options - Lock acquisition options\n * @returns Result of the callback function\n * @throws Error from callback or lock acquisition failure\n *\n * @example\n * ```typescript\n * const result = await processLock.withLock('/tmp/my-lock', async () => {\n * // Critical section\n * return someValue\n * })\n * ```\n */\n async withLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options?: ProcessLockOptions,\n ): Promise<T> {\n const release = await this.acquire(lockPath, options)\n try {\n return await fn()\n } finally {\n release()\n }\n }\n}\n\n// Export singleton instance.\nexport const processLock = new ProcessLockManager()\n"],
5
+ "mappings": ";4ZAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,iBAAAE,IAAA,eAAAC,EAAAH,GAKA,IAAAI,EAAgD,mBAEhDC,EAA+B,gBAC/BC,EAAuB,oBACvBC,EAAuB,sBACvBC,EAAuB,yBAsCvB,MAAMC,CAAmB,CACf,YAAc,IAAI,IAClB,sBAAwB,GAMxB,mBAAoB,CACtB,KAAK,2BAIT,UAAO,IAAM,CACX,UAAWC,KAAY,KAAK,YAC1B,GAAI,IACE,cAAWA,CAAQ,MACrB,kBAAeA,EAAU,CAAE,UAAW,EAAK,CAAC,CAEhD,MAAQ,CAER,CAEJ,CAAC,EAED,KAAK,sBAAwB,GAC/B,CAWQ,QAAQA,EAAkBC,EAA0B,CAC1D,GAAI,CACF,GAAI,IAAC,cAAWD,CAAQ,EACtB,MAAO,GAGT,MAAME,KAAQ,YAASF,CAAQ,EAE/B,OADY,KAAK,IAAI,EAAIE,EAAM,MAAM,QAAQ,EAChCD,CACf,MAAQ,CACN,MAAO,EACT,CACF,CAyBA,MAAM,QACJD,EACAG,EAA8B,CAAC,EACV,CACrB,KAAM,CACJ,YAAAC,EAAc,IACd,WAAAC,EAAa,IACb,QAAAC,EAAU,EACV,QAAAL,EAAU,GACZ,EAAIE,EAGJ,YAAK,kBAAkB,EAEhB,QAAM,UACX,SAAY,CACV,GAAI,CAEF,MAAI,cAAWH,CAAQ,GAAK,KAAK,QAAQA,EAAUC,CAAO,EAAG,CAC3D,SAAO,IAAI,wBAAwBD,CAAQ,EAAE,EAC7C,GAAI,IACF,kBAAeA,EAAU,CAAE,UAAW,EAAK,CAAC,CAC9C,MAAQ,CAER,CACF,CAGA,sBAAUA,EAAU,CAAE,UAAW,EAAM,CAAC,EAGxC,KAAK,YAAY,IAAIA,CAAQ,EAGtB,IAAM,KAAK,QAAQA,CAAQ,CACpC,OAASO,EAAO,CAEd,MAAIA,aAAiB,OAAUA,EAAc,OAAS,SAChD,KAAK,QAAQP,EAAUC,CAAO,EAC1B,IAAI,MAAM,wBAAwBD,CAAQ,EAAE,EAE9C,IAAI,MAAM,wBAAwBA,CAAQ,EAAE,EAE9CO,CACR,CACF,EACA,CACE,QAAAD,EACA,YAAAF,EACA,WAAAC,EACA,OAAQ,EACV,CACF,CACF,CAaA,QAAQL,EAAwB,CAC9B,GAAI,IACE,cAAWA,CAAQ,MACrB,kBAAeA,EAAU,CAAE,UAAW,EAAK,CAAC,EAE9C,KAAK,YAAY,OAAOA,CAAQ,CAClC,OAASO,EAAO,CACd,SAAO,KACL,0BAA0BP,CAAQ,KAAKO,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAC/F,CACF,CACF,CAuBA,MAAM,SACJP,EACAQ,EACAL,EACY,CACZ,MAAMM,EAAU,MAAM,KAAK,QAAQT,EAAUG,CAAO,EACpD,GAAI,CACF,OAAO,MAAMK,EAAG,CAClB,QAAE,CACAC,EAAQ,CACV,CACF,CACF,CAGO,MAAMjB,EAAc,IAAIO",
6
+ "names": ["process_lock_exports", "__export", "processLock", "__toCommonJS", "import_node_fs", "import_fs", "import_logger", "import_promises", "import_signal_exit", "ProcessLockManager", "lockPath", "staleMs", "stats", "options", "baseDelayMs", "maxDelayMs", "retries", "error", "fn", "release"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "license": "MIT",
5
5
  "description": "Core utilities and infrastructure for Socket.dev security tools",
6
6
  "keywords": [
@@ -384,6 +384,10 @@
384
384
  "types": "./plugins/babel-plugin-inline-require-calls.d.ts",
385
385
  "default": "./plugins/babel-plugin-inline-require-calls.js"
386
386
  },
387
+ "./process-lock": {
388
+ "types": "./dist/process-lock.d.ts",
389
+ "default": "./dist/process-lock.js"
390
+ },
387
391
  "./promise-queue": {
388
392
  "types": "./dist/promise-queue.d.ts",
389
393
  "default": "./dist/promise-queue.js"