@socketsecurity/lib 5.20.1 → 5.23.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +116 -95
  2. package/README.md +24 -181
  3. package/dist/archives.js +13 -0
  4. package/dist/cacache.js +6 -8
  5. package/dist/cache-with-ttl.js +1 -1
  6. package/dist/constants/socket.js +1 -1
  7. package/dist/dlx/detect.js +25 -8
  8. package/dist/dlx/manifest.js +8 -19
  9. package/dist/dlx/package.js +16 -2
  10. package/dist/env/socket-cli.d.ts +4 -3
  11. package/dist/env/socket-cli.js +1 -1
  12. package/dist/errors.d.ts +96 -2
  13. package/dist/errors.js +55 -0
  14. package/dist/external/pony-cause.js +12 -11
  15. package/dist/fs.js +8 -2
  16. package/dist/github.js +3 -2
  17. package/dist/globs.js +5 -1
  18. package/dist/ipc.js +2 -2
  19. package/dist/json/edit.js +3 -2
  20. package/dist/json/parse.d.ts +47 -2
  21. package/dist/json/parse.js +40 -2
  22. package/dist/json/types.d.ts +49 -0
  23. package/dist/memoization.d.ts +4 -23
  24. package/dist/memoization.js +14 -54
  25. package/dist/packages/isolation.js +4 -4
  26. package/dist/packages/specs.js +9 -2
  27. package/dist/performance.js +3 -2
  28. package/dist/process-lock.js +4 -12
  29. package/dist/promise-queue.d.ts +9 -4
  30. package/dist/promise-queue.js +9 -7
  31. package/dist/promises.d.ts +41 -0
  32. package/dist/promises.js +19 -2
  33. package/dist/regexps.d.ts +4 -13
  34. package/dist/regexps.js +60 -3
  35. package/dist/releases/github.js +3 -2
  36. package/dist/releases/socket-btm.d.ts +61 -5
  37. package/dist/releases/socket-btm.js +2 -2
  38. package/dist/schema/parse.d.ts +26 -0
  39. package/dist/schema/parse.js +38 -0
  40. package/dist/schema/types.d.ts +121 -0
  41. package/dist/schema/validate.d.ts +35 -0
  42. package/dist/{validation/validate-schema.js → schema/validate.js} +4 -14
  43. package/dist/suppress-warnings.js +0 -2
  44. package/dist/url.js +5 -1
  45. package/dist/versions.js +2 -2
  46. package/dist/words.js +4 -7
  47. package/package.json +15 -15
  48. package/dist/validation/json-parser.d.ts +0 -58
  49. package/dist/validation/json-parser.js +0 -63
  50. package/dist/validation/types.d.ts +0 -118
  51. package/dist/validation/validate-schema.d.ts +0 -124
  52. /package/dist/{validation → schema}/types.js +0 -0
@@ -21,9 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var parse_exports = {};
22
22
  __export(parse_exports, {
23
23
  isJsonPrimitive: () => isJsonPrimitive,
24
- jsonParse: () => jsonParse
24
+ jsonParse: () => jsonParse,
25
+ safeJsonParse: () => safeJsonParse
25
26
  });
26
27
  module.exports = __toCommonJS(parse_exports);
28
+ var import_validate = require("../schema/validate");
27
29
  var import_strings = require("../strings");
28
30
  const JSONParse = JSON.parse;
29
31
  // @__NO_SIDE_EFFECTS__
@@ -69,8 +71,44 @@ function jsonParse(content, options) {
69
71
  }
70
72
  return void 0;
71
73
  }
74
+ const DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
75
+ function prototypePollutionReviver(key, value) {
76
+ if (DANGEROUS_KEYS.has(key)) {
77
+ throw new Error(
78
+ "JSON contains potentially malicious prototype pollution keys"
79
+ );
80
+ }
81
+ return value;
82
+ }
83
+ const DEFAULT_MAX_SIZE = 10 * 1024 * 1024;
84
+ // @__NO_SIDE_EFFECTS__
85
+ function safeJsonParse(jsonString, schema, options = {}) {
86
+ const { allowPrototype = false, maxSize = DEFAULT_MAX_SIZE } = options;
87
+ const byteLength = Buffer.byteLength(jsonString, "utf8");
88
+ if (byteLength > maxSize) {
89
+ throw new Error(
90
+ `JSON string exceeds maximum size limit${maxSize !== DEFAULT_MAX_SIZE ? ` of ${maxSize} bytes` : ""}`
91
+ );
92
+ }
93
+ let parsed;
94
+ try {
95
+ parsed = allowPrototype ? JSONParse(jsonString) : JSONParse(jsonString, prototypePollutionReviver);
96
+ } catch (error) {
97
+ throw new Error(`Failed to parse JSON: ${error}`);
98
+ }
99
+ if (schema) {
100
+ const result = (0, import_validate.validateSchema)(schema, parsed);
101
+ if (!result.ok) {
102
+ const summary = result.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join(", ");
103
+ throw new Error(`Validation failed: ${summary}`);
104
+ }
105
+ return result.value;
106
+ }
107
+ return parsed;
108
+ }
72
109
  // Annotate the CommonJS export names for ESM import in node:
73
110
  0 && (module.exports = {
74
111
  isJsonPrimitive,
75
- jsonParse
112
+ jsonParse,
113
+ safeJsonParse
76
114
  });
@@ -72,6 +72,55 @@ export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
72
72
  * ```
73
73
  */
74
74
  export type JsonReviver = (key: string, value: unknown) => unknown;
75
+ /**
76
+ * Options for `safeJsonParse`: security controls for untrusted JSON.
77
+ *
78
+ * Distinct from `JsonParseOptions` (which is scoped to reviver /
79
+ * error-handling for trusted-source fs reads). Use this type when
80
+ * parsing user input, network payloads, or anything beyond a trust
81
+ * boundary.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const options: SafeJsonParseOptions = {
86
+ * maxSize: 1024 * 1024, // 1MB limit
87
+ * allowPrototype: false // Block prototype pollution
88
+ * }
89
+ * ```
90
+ */
91
+ export interface SafeJsonParseOptions {
92
+ /**
93
+ * Allow dangerous prototype pollution keys (`__proto__`, `constructor`, `prototype`).
94
+ * Set to `true` only if you trust the JSON source completely.
95
+ *
96
+ * @default false
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * // Will throw error by default
101
+ * safeJsonParse('{"__proto__": {"polluted": true}}')
102
+ *
103
+ * // Allows the parse (dangerous!)
104
+ * safeJsonParse('{"__proto__": {"polluted": true}}', undefined, {
105
+ * allowPrototype: true
106
+ * })
107
+ * ```
108
+ */
109
+ allowPrototype?: boolean | undefined;
110
+ /**
111
+ * Maximum allowed size of JSON string in bytes.
112
+ * Prevents memory exhaustion from extremely large payloads.
113
+ *
114
+ * @default 10_485_760 (10 MB)
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * // Limit to 1KB
119
+ * safeJsonParse(jsonString, undefined, { maxSize: 1024 })
120
+ * ```
121
+ */
122
+ maxSize?: number | undefined;
123
+ }
75
124
  /**
76
125
  * Options for JSON parsing operations.
77
126
  */
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Options for memoization behavior.
7
7
  */
8
- export type MemoizeOptions<Args extends unknown[], _Result = unknown> = {
8
+ export type MemoizeOptions<Args extends unknown[]> = {
9
9
  /** Custom cache key generator (defaults to JSON.stringify) */
10
10
  keyGen?: (...args: Args) => string;
11
11
  /** Maximum cache size (LRU eviction when exceeded) */
@@ -49,7 +49,7 @@ export declare function clearAllMemoizationCaches(): void;
49
49
  * }
50
50
  * }
51
51
  */
52
- export declare function Memoize(options?: MemoizeOptions<unknown[], unknown>): (_target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
52
+ export declare function Memoize(options?: MemoizeOptions<unknown[]>): (_target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
53
53
  /**
54
54
  * Memoize a function with configurable caching behavior.
55
55
  * Caches function results to avoid repeated computation.
@@ -69,7 +69,7 @@ export declare function Memoize(options?: MemoizeOptions<unknown[], unknown>): (
69
69
  * expensiveOperation(1000) // Computed
70
70
  * expensiveOperation(1000) // Cached
71
71
  */
72
- export declare function memoize<Args extends unknown[], Result>(fn: (...args: Args) => Result, options?: MemoizeOptions<Args, Result>): (...args: Args) => Result;
72
+ export declare function memoize<Args extends unknown[], Result>(fn: (...args: Args) => Result, options?: MemoizeOptions<Args>): (...args: Args) => Result;
73
73
  /**
74
74
  * Memoize an async function.
75
75
  * Similar to memoize() but handles promises properly.
@@ -89,26 +89,7 @@ export declare function memoize<Args extends unknown[], Result>(fn: (...args: Ar
89
89
  * await fetchUser('123') // Fetches from API
90
90
  * await fetchUser('123') // Returns cached result
91
91
  */
92
- export declare function memoizeAsync<Args extends unknown[], Result>(fn: (...args: Args) => Promise<Result>, options?: MemoizeOptions<Args, Result>): (...args: Args) => Promise<Result>;
93
- /**
94
- * Create a debounced memoized function.
95
- * Combines memoization with debouncing for expensive operations.
96
- *
97
- * @param fn - Function to memoize and debounce
98
- * @param wait - Debounce wait time in milliseconds
99
- * @param options - Memoization options
100
- * @returns Debounced memoized function
101
- *
102
- * @example
103
- * import { memoizeDebounced } from '@socketsecurity/lib/memoization'
104
- *
105
- * const search = memoizeDebounced(
106
- * (query: string) => performSearch(query),
107
- * 300,
108
- * { name: 'search' }
109
- * )
110
- */
111
- export declare function memoizeDebounced<Args extends unknown[], Result>(fn: (...args: Args) => Result, wait: number, options?: MemoizeOptions<Args, Result>): (...args: Args) => Result;
92
+ export declare function memoizeAsync<Args extends unknown[], Result>(fn: (...args: Args) => Promise<Result>, options?: MemoizeOptions<Args>): (...args: Args) => Promise<Result>;
112
93
  /**
113
94
  * Memoize with WeakMap for object keys.
114
95
  * Allows garbage collection when objects are no longer referenced.
@@ -24,7 +24,6 @@ __export(memoization_exports, {
24
24
  clearAllMemoizationCaches: () => clearAllMemoizationCaches,
25
25
  memoize: () => memoize,
26
26
  memoizeAsync: () => memoizeAsync,
27
- memoizeDebounced: () => memoizeDebounced,
28
27
  memoizeWeak: () => memoizeWeak,
29
28
  once: () => once
30
29
  });
@@ -78,15 +77,13 @@ function memoize(fn, options = {}) {
78
77
  throw new TypeError("TTL must be non-negative");
79
78
  }
80
79
  const cache = /* @__PURE__ */ new Map();
81
- const accessOrder = [];
82
80
  cacheRegistry.push(() => {
83
81
  cache.clear();
84
- accessOrder.length = 0;
85
82
  });
86
83
  function evictLRU() {
87
- if (cache.size >= maxSize && accessOrder.length > 0) {
88
- const oldest = accessOrder.shift();
89
- if (oldest) {
84
+ if (cache.size >= maxSize) {
85
+ const oldest = cache.keys().next().value;
86
+ if (oldest !== void 0) {
90
87
  cache.delete(oldest);
91
88
  (0, import_debug.debugLog)(`[memoize:${name}] clear`, {
92
89
  key: oldest,
@@ -107,19 +104,12 @@ function memoize(fn, options = {}) {
107
104
  if (cached) {
108
105
  if (!isExpired(cached)) {
109
106
  cached.hits++;
110
- const index2 = accessOrder.indexOf(key);
111
- if (index2 !== -1) {
112
- accessOrder.splice(index2, 1);
113
- }
114
- accessOrder.push(key);
107
+ cache.delete(key);
108
+ cache.set(key, cached);
115
109
  (0, import_debug.debugLog)(`[memoize:${name}] hit`, { key, hits: cached.hits });
116
110
  return cached.value;
117
111
  }
118
112
  cache.delete(key);
119
- const index = accessOrder.indexOf(key);
120
- if (index !== -1) {
121
- accessOrder.splice(index, 1);
122
- }
123
113
  }
124
114
  (0, import_debug.debugLog)(`[memoize:${name}] miss`, { key });
125
115
  const value = fn(...args);
@@ -129,7 +119,6 @@ function memoize(fn, options = {}) {
129
119
  timestamp: Date.now(),
130
120
  hits: 0
131
121
  });
132
- accessOrder.push(key);
133
122
  (0, import_debug.debugLog)(`[memoize:${name}] set`, { key, cacheSize: cache.size });
134
123
  return value;
135
124
  };
@@ -142,15 +131,13 @@ function memoizeAsync(fn, options = {}) {
142
131
  ttl = Number.POSITIVE_INFINITY
143
132
  } = options;
144
133
  const cache = /* @__PURE__ */ new Map();
145
- const accessOrder = [];
146
134
  cacheRegistry.push(() => {
147
135
  cache.clear();
148
- accessOrder.length = 0;
149
136
  });
150
137
  function evictLRU() {
151
- if (cache.size >= maxSize && accessOrder.length > 0) {
152
- const oldest = accessOrder.shift();
153
- if (oldest) {
138
+ if (cache.size >= maxSize) {
139
+ const oldest = cache.keys().next().value;
140
+ if (oldest !== void 0) {
154
141
  cache.delete(oldest);
155
142
  (0, import_debug.debugLog)(`[memoizeAsync:${name}] clear`, {
156
143
  key: oldest,
@@ -165,6 +152,10 @@ function memoizeAsync(fn, options = {}) {
165
152
  }
166
153
  return Date.now() - entry.timestamp > ttl;
167
154
  }
155
+ function bumpRecency(key, entry) {
156
+ cache.delete(key);
157
+ cache.set(key, entry);
158
+ }
168
159
  const refreshing = /* @__PURE__ */ new Map();
169
160
  return async function memoized(...args) {
170
161
  const key = keyGen(...args);
@@ -172,29 +163,17 @@ function memoizeAsync(fn, options = {}) {
172
163
  if (cached) {
173
164
  if (!isExpired(cached)) {
174
165
  cached.hits++;
175
- const index2 = accessOrder.indexOf(key);
176
- if (index2 !== -1) {
177
- accessOrder.splice(index2, 1);
178
- }
179
- accessOrder.push(key);
166
+ bumpRecency(key, cached);
180
167
  (0, import_debug.debugLog)(`[memoizeAsync:${name}] hit`, { key, hits: cached.hits });
181
168
  return await cached.value;
182
169
  }
183
170
  const inflight = refreshing.get(key);
184
171
  if (inflight) {
185
172
  (0, import_debug.debugLog)(`[memoizeAsync:${name}] stale-dedup`, { key });
186
- const inflightIndex = accessOrder.indexOf(key);
187
- if (inflightIndex !== -1) {
188
- accessOrder.splice(inflightIndex, 1);
189
- }
190
- accessOrder.push(key);
173
+ bumpRecency(key, cached);
191
174
  return await inflight;
192
175
  }
193
176
  cache.delete(key);
194
- const index = accessOrder.indexOf(key);
195
- if (index !== -1) {
196
- accessOrder.splice(index, 1);
197
- }
198
177
  }
199
178
  (0, import_debug.debugLog)(`[memoizeAsync:${name}] miss`, { key });
200
179
  const promise = fn(...args).then(
@@ -210,10 +189,6 @@ function memoizeAsync(fn, options = {}) {
210
189
  (error) => {
211
190
  refreshing.delete(key);
212
191
  cache.delete(key);
213
- const index = accessOrder.indexOf(key);
214
- if (index !== -1) {
215
- accessOrder.splice(index, 1);
216
- }
217
192
  (0, import_debug.debugLog)(`[memoizeAsync:${name}] error`, { key, error });
218
193
  throw error;
219
194
  }
@@ -225,24 +200,10 @@ function memoizeAsync(fn, options = {}) {
225
200
  timestamp: Date.now(),
226
201
  hits: 0
227
202
  });
228
- accessOrder.push(key);
229
203
  (0, import_debug.debugLog)(`[memoizeAsync:${name}] set`, { key, cacheSize: cache.size });
230
204
  return await promise;
231
205
  };
232
206
  }
233
- function memoizeDebounced(fn, wait, options = {}) {
234
- const memoized = memoize(fn, options);
235
- let timeoutId;
236
- return function debounced(...args) {
237
- if (timeoutId) {
238
- clearTimeout(timeoutId);
239
- }
240
- timeoutId = setTimeout(() => {
241
- memoized(...args);
242
- }, wait);
243
- return memoized(...args);
244
- };
245
- }
246
207
  function memoizeWeak(fn) {
247
208
  const cache = /* @__PURE__ */ new WeakMap();
248
209
  return function memoized(key) {
@@ -276,7 +237,6 @@ function once(fn) {
276
237
  clearAllMemoizationCaches,
277
238
  memoize,
278
239
  memoizeAsync,
279
- memoizeDebounced,
280
240
  memoizeWeak,
281
241
  once
282
242
  });
@@ -35,6 +35,7 @@ __export(isolation_exports, {
35
35
  module.exports = __toCommonJS(isolation_exports);
36
36
  var import_npm_package_arg = __toESM(require("../external/npm-package-arg"));
37
37
  var import_platform = require("../constants/platform");
38
+ var import_errors = require("../errors");
38
39
  var import_normalize = require("../paths/normalize");
39
40
  var import_socket = require("../paths/socket");
40
41
  var import_spawn = require("../spawn");
@@ -69,10 +70,9 @@ async function mergePackageJson(pkgJsonPath, originalPkgJson) {
69
70
  try {
70
71
  pkgJson = JSON.parse(await fs.promises.readFile(pkgJsonPath, "utf8"));
71
72
  } catch (error) {
72
- throw new Error(
73
- `Failed to parse ${pkgJsonPath}: ${error instanceof Error ? error.message : String(error)}`,
74
- { cause: error }
75
- );
73
+ throw new Error(`Failed to parse ${pkgJsonPath}: ${(0, import_errors.errorMessage)(error)}`, {
74
+ cause: error
75
+ });
76
76
  }
77
77
  const mergedPkgJson = originalPkgJson ? { ...originalPkgJson, ...pkgJson } : pkgJson;
78
78
  return mergedPkgJson;
@@ -42,9 +42,16 @@ var import_objects = require("../objects");
42
42
  var import_strings = require("../strings");
43
43
  // @__NO_SIDE_EFFECTS__
44
44
  function getRepoUrlDetails(repoUrl = "") {
45
- const userAndRepo = repoUrl.replace(/^.+github.com\//, "").split("/");
45
+ const match = /^(?:[a-z][a-z+]*:\/\/)(?:[^/@]+@)?github\.com\/([^?#]+)(?:[?#]|$)/i.exec(
46
+ repoUrl
47
+ );
48
+ if (!match || !match[1]) {
49
+ return { user: "", project: "" };
50
+ }
51
+ const userAndRepo = match[1].split("/");
46
52
  const user = userAndRepo[0] || "";
47
- const project = userAndRepo.length > 1 ? (userAndRepo[1]?.endsWith(".git") ? userAndRepo[1].slice(0, -4) : userAndRepo[1]) || "" : "";
53
+ const rawProject = userAndRepo[1] ?? "";
54
+ const project = rawProject.endsWith(".git") ? rawProject.slice(0, -4) : rawProject;
48
55
  return { user, project };
49
56
  }
50
57
  // @__NO_SIDE_EFFECTS__
@@ -44,6 +44,7 @@ __export(performance_exports, {
44
44
  module.exports = __toCommonJS(performance_exports);
45
45
  var import_node_process = __toESM(require("node:process"));
46
46
  var import_debug = require("./debug");
47
+ var import_errors = require("./errors");
47
48
  const performanceMetrics = [];
48
49
  function isPerfEnabled() {
49
50
  return import_node_process.default.env["DEBUG"]?.includes("perf") || false;
@@ -128,7 +129,7 @@ async function measure(operation, fn, metadata) {
128
129
  } catch (e) {
129
130
  stop({
130
131
  success: false,
131
- error: e instanceof Error ? e.message : "Unknown"
132
+ error: (0, import_errors.errorMessage)(e)
132
133
  });
133
134
  throw e;
134
135
  }
@@ -143,7 +144,7 @@ function measureSync(operation, fn, metadata) {
143
144
  } catch (e) {
144
145
  stop({
145
146
  success: false,
146
- error: e instanceof Error ? e.message : "Unknown"
147
+ error: (0, import_errors.errorMessage)(e)
147
148
  });
148
149
  throw e;
149
150
  }
@@ -23,6 +23,7 @@ __export(process_lock_exports, {
23
23
  processLock: () => processLock
24
24
  });
25
25
  module.exports = __toCommonJS(process_lock_exports);
26
+ var import_errors = require("./errors");
26
27
  var import_fs = require("./fs");
27
28
  var import_logger = require("./logger");
28
29
  var import_promises = require("./promises");
@@ -87,9 +88,7 @@ class ProcessLockManager {
87
88
  fs.utimesSync(lockPath, now, now);
88
89
  }
89
90
  } catch (error) {
90
- logger.warn(
91
- `Failed to touch lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`
92
- );
91
+ logger.warn(`Failed to touch lock ${lockPath}: ${(0, import_errors.errorMessage)(error)}`);
93
92
  }
94
93
  }
95
94
  /**
@@ -136,9 +135,7 @@ class ProcessLockManager {
136
135
  if (!stats) {
137
136
  return false;
138
137
  }
139
- const ageSeconds = Math.floor((Date.now() - stats.mtime.getTime()) / 1e3);
140
- const staleSeconds = Math.floor(staleMs / 1e3);
141
- return ageSeconds > staleSeconds;
138
+ return Date.now() - stats.mtime.getTime() > staleMs;
142
139
  } catch {
143
140
  return false;
144
141
  }
@@ -186,9 +183,6 @@ class ProcessLockManager {
186
183
  }
187
184
  }
188
185
  const fs = /* @__PURE__ */ getFs();
189
- if (fs.existsSync(lockPath)) {
190
- throw new Error(`Lock already exists: ${lockPath}`);
191
- }
192
186
  const parent = (/* @__PURE__ */ getPath()).dirname(lockPath);
193
187
  if (parent && parent !== "." && parent !== lockPath) {
194
188
  fs.mkdirSync(parent, { recursive: true });
@@ -282,9 +276,7 @@ To resolve:
282
276
  }
283
277
  this.activeLocks.delete(lockPath);
284
278
  } catch (error) {
285
- logger.warn(
286
- `Failed to release lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`
287
- );
279
+ logger.warn(`Failed to release lock ${lockPath}: ${(0, import_errors.errorMessage)(error)}`);
288
280
  }
289
281
  }
290
282
  /**
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @fileoverview Bounded concurrency promise queue.
3
3
  * Exports the `PromiseQueue` class, which limits how many async tasks run
4
- * simultaneously, supports an optional max queue length (dropping the oldest
5
- * pending task when exceeded), and exposes an idle-wait helper.
4
+ * simultaneously, supports an optional max queue length (new tasks beyond
5
+ * the cap are rejected with "Task dropped: queue length exceeded"), and
6
+ * exposes an idle-wait helper.
6
7
  */
7
8
  export declare class PromiseQueue {
8
9
  private queue;
@@ -13,13 +14,17 @@ export declare class PromiseQueue {
13
14
  /**
14
15
  * Creates a new PromiseQueue
15
16
  * @param maxConcurrency - Maximum number of promises that can run concurrently
16
- * @param maxQueueLength - Maximum queue size (older tasks are dropped if exceeded)
17
+ * @param maxQueueLength - Maximum queue size; submissions past the cap
18
+ * reject with "Task dropped: queue length exceeded" instead of evicting
19
+ * a caller that has been waiting patiently. Callers must handle this
20
+ * rejection or they'll see an unhandled rejection.
17
21
  */
18
22
  constructor(maxConcurrency: number, maxQueueLength?: number | undefined);
19
23
  /**
20
24
  * Add a task to the queue
21
25
  * @param fn - Async function to execute
22
- * @returns Promise that resolves with the function's result
26
+ * @returns Promise that resolves with the function's result, or rejects
27
+ * with "Task dropped: queue length exceeded" if the queue is full.
23
28
  */
24
29
  add<T>(fn: () => Promise<T>): Promise<T>;
25
30
  private runNext;
@@ -32,7 +32,10 @@ class PromiseQueue {
32
32
  /**
33
33
  * Creates a new PromiseQueue
34
34
  * @param maxConcurrency - Maximum number of promises that can run concurrently
35
- * @param maxQueueLength - Maximum queue size (older tasks are dropped if exceeded)
35
+ * @param maxQueueLength - Maximum queue size; submissions past the cap
36
+ * reject with "Task dropped: queue length exceeded" instead of evicting
37
+ * a caller that has been waiting patiently. Callers must handle this
38
+ * rejection or they'll see an unhandled rejection.
36
39
  */
37
40
  constructor(maxConcurrency, maxQueueLength) {
38
41
  this.maxConcurrency = maxConcurrency;
@@ -44,17 +47,16 @@ class PromiseQueue {
44
47
  /**
45
48
  * Add a task to the queue
46
49
  * @param fn - Async function to execute
47
- * @returns Promise that resolves with the function's result
50
+ * @returns Promise that resolves with the function's result, or rejects
51
+ * with "Task dropped: queue length exceeded" if the queue is full.
48
52
  */
49
53
  async add(fn) {
50
54
  return await new Promise((resolve, reject) => {
51
- const task = { fn, resolve, reject };
52
55
  if (this.maxQueueLength !== void 0 && this.queue.length >= this.maxQueueLength) {
53
- const droppedTask = this.queue.shift();
54
- if (droppedTask) {
55
- droppedTask.reject(new Error("Task dropped: queue length exceeded"));
56
- }
56
+ reject(new Error("Task dropped: queue length exceeded"));
57
+ return;
57
58
  }
59
+ const task = { fn, resolve, reject };
58
60
  this.queue.push(task);
59
61
  this.runNext();
60
62
  });
@@ -440,3 +440,44 @@ export declare function pRetry<T>(callbackFn: (...args: unknown[]) => Promise<T>
440
440
  * // => { retries: 5, minTimeout: 200, maxTimeout: 5000, factor: 2 }
441
441
  */
442
442
  export declare function resolveRetryOptions(options?: number | RetryOptions | undefined): RetryOptions;
443
+ /**
444
+ * Shape returned by {@link withResolvers}: a fresh pending promise plus
445
+ * the `resolve` / `reject` handles that settle it.
446
+ *
447
+ * Matches the spec return-shape exactly
448
+ * ([ECMA-262 §27.2.4.9](https://tc39.es/ecma262/#sec-promise.withResolvers)).
449
+ */
450
+ export interface PromiseWithResolvers<T> {
451
+ /** The pending promise. */
452
+ promise: Promise<T>;
453
+ /** Resolves {@link promise} with the given value (or thenable). */
454
+ resolve: (value: T | PromiseLike<T>) => void;
455
+ /** Rejects {@link promise} with the given reason. */
456
+ reject: (reason?: unknown) => void;
457
+ }
458
+ /**
459
+ * Create a pending promise together with its `resolve` and `reject`
460
+ * handles as first-class values, per
461
+ * [ECMA-262 §27.2.4.9](https://tc39.es/ecma262/#sec-promise.withResolvers).
462
+ *
463
+ * Bound to native `Promise.withResolvers` when available (Node 20.12+ /
464
+ * 21+ / 22+; V8 ≥ 12.0); otherwise falls back to a spec-equivalent
465
+ * `new Promise(executor)` implementation that captures the handles via
466
+ * closure. The returned object always has own data properties `promise`,
467
+ * `resolve`, `reject` on `Object.prototype` — writable, enumerable, and
468
+ * configurable — matching the spec's `CreateDataPropertyOrThrow` steps.
469
+ *
470
+ * Use this instead of the manual
471
+ * `let resolve; const p = new Promise(r => { resolve = r })` dance for
472
+ * deferred-resolution patterns (event-driven bridges, adapter layers,
473
+ * handshake signaling) where the settle path lives outside the executor.
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * const { promise, resolve, reject } = withResolvers<string>()
478
+ * emitter.once('ready', () => resolve('ok'))
479
+ * emitter.once('error', err => reject(err))
480
+ * const result = await promise
481
+ * ```
482
+ */
483
+ export declare const withResolvers: <T>() => PromiseWithResolvers<T>;
package/dist/promises.js CHANGED
@@ -27,7 +27,8 @@ __export(promises_exports, {
27
27
  pFilter: () => pFilter,
28
28
  pFilterChunk: () => pFilterChunk,
29
29
  pRetry: () => pRetry,
30
- resolveRetryOptions: () => resolveRetryOptions
30
+ resolveRetryOptions: () => resolveRetryOptions,
31
+ withResolvers: () => withResolvers
31
32
  });
32
33
  module.exports = __toCommonJS(promises_exports);
33
34
  var import_arrays = require("./arrays");
@@ -258,6 +259,21 @@ function resolveRetryOptions(options) {
258
259
  }
259
260
  return options ? { ...defaults, ...options } : defaults;
260
261
  }
262
+ const maybeNativeWithResolvers = Promise.withResolvers;
263
+ const withResolvers = typeof maybeNativeWithResolvers === "function" ? (
264
+ // Bind so callers who destructure the export don't lose `this`.
265
+ maybeNativeWithResolvers.bind(
266
+ Promise
267
+ )
268
+ ) : () => {
269
+ let resolve;
270
+ let reject;
271
+ const promise = new Promise((res, rej) => {
272
+ resolve = res;
273
+ reject = rej;
274
+ });
275
+ return { promise, resolve, reject };
276
+ };
261
277
  // Annotate the CommonJS export names for ESM import in node:
262
278
  0 && (module.exports = {
263
279
  normalizeIterationOptions,
@@ -267,5 +283,6 @@ function resolveRetryOptions(options) {
267
283
  pFilter,
268
284
  pFilterChunk,
269
285
  pRetry,
270
- resolveRetryOptions
286
+ resolveRetryOptions,
287
+ withResolvers
271
288
  });
package/dist/regexps.d.ts CHANGED
@@ -1,15 +1,6 @@
1
1
  /**
2
- * @fileoverview Regular expression utilities including escape-string-regexp implementation.
3
- * Provides regex escaping and pattern matching helpers.
2
+ * @fileoverview Regular expression utilities including a spec-compliant
3
+ * `RegExp.escape` fallback. Provides regex escaping and pattern matching
4
+ * helpers.
4
5
  */
5
- /**
6
- * Escape special characters in a string for use in a regular expression.
7
- *
8
- * @example
9
- * ```typescript
10
- * escapeRegExp('foo.bar') // 'foo\\.bar'
11
- * escapeRegExp('a+b*c?') // 'a\\+b\\*c\\?'
12
- * new RegExp(escapeRegExp('[test]')) // /\[test\]/
13
- * ```
14
- */
15
- export declare function escapeRegExp(str: string): string;
6
+ export declare const escapeRegExp: (str: string) => string;