happy-rusty 1.9.2 → 1.10.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,15 @@ 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
+ ## [1.10.0] - 2026-06-30
9
+
10
+ ### Added
11
+ - **New Sync Primitive**: `Semaphore` - Counting semaphore for limiting async concurrency, with `acquire`/`tryAcquire`/`withPermit`/`availablePermits`. Inspired by tokio's `Semaphore` (Rust std does not include one). Useful for fetch rate limiting, connection pools, and task queues.
12
+ - **New Method**: `RwLockWriteGuard.downgrade()` - Atomically converts a write guard to a read guard, releasing waiting readers while keeping pending writers waiting. Equivalent to Rust 1.92's `RwLockWriteGuard::downgrade`
13
+
14
+ ### Chores
15
+ - Upgraded devDependencies: vite, vitest, @vitest/coverage-v8, @vitest/ui, eslint, rollup, typescript-eslint
16
+
8
17
  ## [1.9.2] - 2026-04-12
9
18
 
10
19
  ### Changed
@@ -294,6 +303,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
294
303
  - Full TypeScript support
295
304
  - Comprehensive API matching Rust's Option and Result
296
305
 
306
+ [1.10.0]: https://github.com/JiangJie/happy-rusty/compare/v1.9.2...v1.10.0
297
307
  [1.9.2]: https://github.com/JiangJie/happy-rusty/compare/v1.9.1...v1.9.2
298
308
  [1.9.1]: https://github.com/JiangJie/happy-rusty/compare/v1.9.0...v1.9.1
299
309
  [1.9.0]: https://github.com/JiangJie/happy-rusty/compare/v1.8.0...v1.9.0
package/README.md CHANGED
@@ -20,7 +20,7 @@ Rust's `Option`, `Result`, and sync primitives for JavaScript/TypeScript - Bette
20
20
 
21
21
  - **Option<T>** - Represents an optional value: every `Option` is either `Some(T)` or `None`
22
22
  - **Result<T, E>** - Represents either success (`Ok(T)`) or failure (`Err(E)`)
23
- - **Sync Primitives** - Rust-inspired `Once<T>`, `OnceAsync<T>`, `Lazy<T>`, `LazyAsync<T>`, `Mutex<T>`, `RwLock<T>`, and `Channel<T>`
23
+ - **Sync Primitives** - Rust-inspired `Once<T>`, `OnceAsync<T>`, `Lazy<T>`, `LazyAsync<T>`, `Mutex<T>`, `RwLock<T>`, `Semaphore`, and `Channel<T>`
24
24
  - **Control Flow** - `ControlFlow<B, C>` with `Break` and `Continue` for short-circuiting operations
25
25
  - **FnOnce** - One-time callable function wrappers (`FnOnce` and `FnOnceAsync`)
26
26
  - **Full TypeScript support** with strict type inference
@@ -137,7 +137,7 @@ const response = await tryAsyncResult(fetch, '/api/data');
137
137
  ### Sync Primitives
138
138
 
139
139
  ```ts
140
- import { Lazy, LazyAsync, Mutex, Channel } from 'happy-rusty';
140
+ import { Lazy, LazyAsync, Mutex, Semaphore, Channel } from 'happy-rusty';
141
141
 
142
142
  // Lazy - compute once on first access
143
143
  const expensive = Lazy(() => computeExpensiveValue());
@@ -151,6 +151,10 @@ await db.force(); // Only one connection, concurrent calls wait
151
151
  const state = Mutex({ count: 0 });
152
152
  await state.withLock(async (s) => { s.count += 1; });
153
153
 
154
+ // Semaphore - limit async concurrency (e.g. fetch rate limiting)
155
+ const sem = Semaphore(10);
156
+ await sem.withPermit(() => fetch(url));
157
+
154
158
  // Channel - MPMC async message passing
155
159
  const ch = Channel<string>(10); // bounded capacity
156
160
  await ch.send('hello');
@@ -165,6 +169,7 @@ for await (const msg of ch) { console.log(msg); }
165
169
  - [Lazy](examples/std/sync/lazy.ts) / [LazyAsync](examples/std/sync/lazy_async.ts)
166
170
  - [Mutex](examples/std/sync/mutex.ts)
167
171
  - [RwLock](examples/std/sync/rwlock.ts)
172
+ - [Semaphore](examples/std/sync/semaphore.ts)
168
173
  - [Channel](examples/std/sync/channel.ts)
169
174
  - [ControlFlow](examples/std/ops/control_flow.ts)
170
175
  - [FnOnce](examples/std/ops/fn_once.ts) / [FnOnceAsync](examples/std/ops/fn_once_async.ts)
@@ -173,7 +178,7 @@ for await (const msg of ch) { console.log(msg); }
173
178
 
174
179
  ### Immutability
175
180
 
176
- All types (`Option`, `Result`, `ControlFlow`, `Lazy`, `LazyAsync`, `Once`, `OnceAsync`, `Mutex`, `MutexGuard`, `RwLock`, `Channel`, `Sender`, `Receiver`, `FnOnce`, `FnOnceAsync`) are **immutable at runtime** via `Object.freeze()`. This prevents accidental modification of methods or properties:
181
+ All types (`Option`, `Result`, `ControlFlow`, `Lazy`, `LazyAsync`, `Once`, `OnceAsync`, `Mutex`, `MutexGuard`, `RwLock`, `Semaphore`, `SemaphorePermit`, `Channel`, `Sender`, `Receiver`, `FnOnce`, `FnOnceAsync`) are **immutable at runtime** via `Object.freeze()`. This prevents accidental modification of methods or properties:
177
182
 
178
183
  ```ts
179
184
  const some = Some(42);
package/dist/main.cjs CHANGED
@@ -22,7 +22,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
22
22
  *
23
23
  * @internal
24
24
  */
25
- const OptionKindSymbol = /* @__PURE__ */ Symbol("Option kind");
25
+ const OptionKindSymbol = /*#__PURE__*/ Symbol("Option kind");
26
26
  //#endregion
27
27
  //#region src/core/option/guards.ts
28
28
  /**
@@ -67,7 +67,7 @@ function isOption(o) {
67
67
  *
68
68
  * @internal
69
69
  */
70
- const ResultKindSymbol = /* @__PURE__ */ Symbol("Result kind");
70
+ const ResultKindSymbol = /*#__PURE__*/ Symbol("Result kind");
71
71
  //#endregion
72
72
  //#region src/core/result/guards.ts
73
73
  /**
@@ -102,8 +102,8 @@ function isResult(r) {
102
102
  * - `Err<T, E>(error)` - Creates a failed Result
103
103
  * - `None` interface - Type overrides for better type inference
104
104
  */
105
- const ASYNC_TRUE$1 = /* @__PURE__ */ Promise.resolve(true);
106
- const ASYNC_FALSE$1 = /* @__PURE__ */ Promise.resolve(false);
105
+ const ASYNC_TRUE$1 = /*#__PURE__*/ Promise.resolve(true);
106
+ const ASYNC_FALSE$1 = /*#__PURE__*/ Promise.resolve(false);
107
107
  /**
108
108
  * Creates an `Option<T>` representing the presence of a value.
109
109
  * This function is typically used to construct an `Option` that contains a value, indicating that the operation yielding the value was successful.
@@ -264,7 +264,7 @@ function Some(value) {
264
264
  * const name = None.unwrapOr('Anonymous'); // 'Anonymous'
265
265
  * ```
266
266
  */
267
- const None = /* @__PURE__ */ Object.freeze({
267
+ const None = /*#__PURE__*/ Object.freeze({
268
268
  [Symbol.toStringTag]: "Option",
269
269
  [OptionKindSymbol]: "None",
270
270
  *[Symbol.iterator]() {},
@@ -403,7 +403,7 @@ const None = /* @__PURE__ */ Object.freeze({
403
403
  * }
404
404
  * ```
405
405
  */
406
- const ASYNC_NONE = /* @__PURE__ */ Promise.resolve(None);
406
+ const ASYNC_NONE = /*#__PURE__*/ Promise.resolve(None);
407
407
  function Ok(value) {
408
408
  const ok = Object.freeze({
409
409
  [Symbol.toStringTag]: "Result",
@@ -803,7 +803,7 @@ async function tryAsyncOption(task, ...args) {
803
803
  * const value: boolean = RESULT_TRUE.intoOk(); // Safe extraction
804
804
  * ```
805
805
  */
806
- const RESULT_TRUE = /* @__PURE__ */ Ok(true);
806
+ const RESULT_TRUE = /*#__PURE__*/ Ok(true);
807
807
  /**
808
808
  * Result constant for `false`.
809
809
  * Can be used anywhere due to immutability.
@@ -818,7 +818,7 @@ const RESULT_TRUE = /* @__PURE__ */ Ok(true);
818
818
  * const value: boolean = RESULT_FALSE.intoOk(); // Safe extraction
819
819
  * ```
820
820
  */
821
- const RESULT_FALSE = /* @__PURE__ */ Ok(false);
821
+ const RESULT_FALSE = /*#__PURE__*/ Ok(false);
822
822
  /**
823
823
  * Result constant for `0`.
824
824
  * Can be used anywhere due to immutability.
@@ -833,7 +833,7 @@ const RESULT_FALSE = /* @__PURE__ */ Ok(false);
833
833
  * const value: number = RESULT_ZERO.intoOk(); // Safe extraction
834
834
  * ```
835
835
  */
836
- const RESULT_ZERO = /* @__PURE__ */ Ok(0);
836
+ const RESULT_ZERO = /*#__PURE__*/ Ok(0);
837
837
  /**
838
838
  * Result constant for `void` or `()`.
839
839
  * Can be used anywhere due to immutability.
@@ -848,7 +848,7 @@ const RESULT_ZERO = /* @__PURE__ */ Ok(0);
848
848
  * RESULT_VOID.intoOk(); // Safe extraction (returns undefined)
849
849
  * ```
850
850
  */
851
- const RESULT_VOID = /* @__PURE__ */ Ok();
851
+ const RESULT_VOID = /*#__PURE__*/ Ok();
852
852
  //#endregion
853
853
  //#region src/core/result/extensions.ts
854
854
  /**
@@ -935,7 +935,7 @@ async function tryAsyncResult(task, ...args) {
935
935
  *
936
936
  * @internal
937
937
  */
938
- const ControlFlowKindSymbol = /* @__PURE__ */ Symbol("ControlFlow kind");
938
+ const ControlFlowKindSymbol = /*#__PURE__*/ Symbol("ControlFlow kind");
939
939
  //#endregion
940
940
  //#region src/std/ops/control_flow.ts
941
941
  /**
@@ -1148,8 +1148,8 @@ function isControlFlow(cf) {
1148
1148
  * Supports rendezvous (capacity=0) for synchronous handoff between sender and receiver.
1149
1149
  *
1150
1150
  */
1151
- const ASYNC_TRUE = /* @__PURE__ */ Promise.resolve(true);
1152
- const ASYNC_FALSE = /* @__PURE__ */ Promise.resolve(false);
1151
+ const ASYNC_TRUE = /*#__PURE__*/ Promise.resolve(true);
1152
+ const ASYNC_FALSE = /*#__PURE__*/ Promise.resolve(false);
1153
1153
  /**
1154
1154
  * Creates a new MPMC channel with the specified capacity.
1155
1155
  *
@@ -2215,6 +2215,17 @@ function RwLock(value) {
2215
2215
  if (released) return;
2216
2216
  released = true;
2217
2217
  releaseWrite();
2218
+ },
2219
+ downgrade() {
2220
+ if (released) throw new Error("RwLockWriteGuard has been released");
2221
+ released = true;
2222
+ writer = false;
2223
+ readers = 1;
2224
+ while (readWaitQueue.length > 0) {
2225
+ readers++;
2226
+ readWaitQueue.shift()();
2227
+ }
2228
+ return createReadGuard();
2218
2229
  }
2219
2230
  });
2220
2231
  }
@@ -2303,6 +2314,119 @@ function RwLock(value) {
2303
2314
  });
2304
2315
  }
2305
2316
  //#endregion
2317
+ //#region src/std/sync/semaphore.ts
2318
+ /**
2319
+ * @module
2320
+ * Counting semaphore for limiting async concurrency.
2321
+ *
2322
+ * Inspired by [tokio's `Semaphore`](https://docs.rs/tokio/latest/tokio/sync/struct.Semaphore.html)
2323
+ * (Rust std does not include one). Unlike `Mutex<T>` which binds to a value,
2324
+ * `Semaphore` is a pure concurrency counter: it limits how many async
2325
+ * operations can run concurrently without protecting any data.
2326
+ *
2327
+ * **When to use `Semaphore` vs `Mutex<T>`:**
2328
+ * - Use `Mutex<T>` for exclusive access to a value (n=1, with data)
2329
+ * - Use `Semaphore` to limit concurrency to N (e.g. fetch rate limiting,
2330
+ * connection pools, task queues)
2331
+ *
2332
+ * `Semaphore(1)` behaves like a `Mutex` without a value (a binary semaphore),
2333
+ * but `Mutex<T>` is preferred when you need to protect a value since the
2334
+ * guard provides typed access via `value`.
2335
+ */
2336
+ /**
2337
+ * Creates a new `Semaphore` with the given capacity.
2338
+ *
2339
+ * @param permits - The maximum number of concurrent operations allowed.
2340
+ * Must be a non-negative integer. Use `0` to disallow any
2341
+ * concurrent acquire (acquire will wait forever).
2342
+ * @returns A new `Semaphore` instance.
2343
+ * @throws {RangeError} If `permits` is negative or not an integer.
2344
+ * @example
2345
+ * ```ts
2346
+ * // Limit to 5 concurrent operations
2347
+ * const sem = Semaphore(5);
2348
+ *
2349
+ * // Binary semaphore (equivalent to a value-less Mutex)
2350
+ * const binary = Semaphore(1);
2351
+ * ```
2352
+ *
2353
+ * @example
2354
+ * ```ts
2355
+ * // Task queue: process 2 jobs at a time
2356
+ * const sem = Semaphore(2);
2357
+ *
2358
+ * async function processJob(job: Job) {
2359
+ * return sem.withPermit(async () => {
2360
+ * return await runJob(job);
2361
+ * });
2362
+ * }
2363
+ *
2364
+ * await Promise.all(jobs.map(processJob));
2365
+ * ```
2366
+ */
2367
+ function Semaphore(permits) {
2368
+ if (!Number.isInteger(permits) || permits < 0) throw new RangeError(`Semaphore capacity must be a non-negative integer, got ${permits}`);
2369
+ const capacity = permits;
2370
+ let available = permits;
2371
+ const waitQueue = [];
2372
+ function releasePermit() {
2373
+ if (waitQueue.length > 0) waitQueue.shift()();
2374
+ else available++;
2375
+ }
2376
+ function createPermit() {
2377
+ let released = false;
2378
+ return Object.freeze({
2379
+ [Symbol.toStringTag]: "SemaphorePermit",
2380
+ toString() {
2381
+ return released ? "SemaphorePermit(<released>)" : "SemaphorePermit";
2382
+ },
2383
+ release() {
2384
+ if (released) return;
2385
+ released = true;
2386
+ releasePermit();
2387
+ }
2388
+ });
2389
+ }
2390
+ function acquire() {
2391
+ if (available > 0) {
2392
+ available--;
2393
+ return Promise.resolve(createPermit());
2394
+ }
2395
+ return new Promise((resolve) => {
2396
+ waitQueue.push(() => {
2397
+ resolve(createPermit());
2398
+ });
2399
+ });
2400
+ }
2401
+ function tryAcquire() {
2402
+ if (available > 0) {
2403
+ available--;
2404
+ return Some(createPermit());
2405
+ }
2406
+ return None;
2407
+ }
2408
+ return Object.freeze({
2409
+ [Symbol.toStringTag]: "Semaphore",
2410
+ toString() {
2411
+ return `Semaphore(${available}/${capacity})`;
2412
+ },
2413
+ capacity,
2414
+ async withPermit(fn) {
2415
+ const permit = await acquire();
2416
+ try {
2417
+ return await fn();
2418
+ } finally {
2419
+ permit.release();
2420
+ }
2421
+ },
2422
+ acquire,
2423
+ tryAcquire,
2424
+ availablePermits() {
2425
+ return available;
2426
+ }
2427
+ });
2428
+ }
2429
+ //#endregion
2306
2430
  exports.ASYNC_NONE = ASYNC_NONE;
2307
2431
  exports.Break = Break;
2308
2432
  exports.Channel = Channel;
@@ -2322,6 +2446,7 @@ exports.RESULT_TRUE = RESULT_TRUE;
2322
2446
  exports.RESULT_VOID = RESULT_VOID;
2323
2447
  exports.RESULT_ZERO = RESULT_ZERO;
2324
2448
  exports.RwLock = RwLock;
2449
+ exports.Semaphore = Semaphore;
2325
2450
  exports.Some = Some;
2326
2451
  exports.isControlFlow = isControlFlow;
2327
2452
  exports.isOption = isOption;