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 +10 -0
- package/README.md +8 -3
- package/dist/main.cjs +138 -13
- package/dist/main.cjs.map +1 -1
- package/dist/main.mjs +138 -14
- package/dist/main.mjs.map +1 -1
- package/dist/types.d.ts +299 -2
- package/package.json +10 -10
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 =
|
|
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 =
|
|
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 =
|
|
106
|
-
const ASYNC_FALSE$1 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1152
|
-
const ASYNC_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;
|