effect 2.3.2 → 2.3.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/dist/cjs/Effect.js.map +1 -1
- package/dist/cjs/RateLimiter.js +67 -1
- package/dist/cjs/RateLimiter.js.map +1 -1
- package/dist/cjs/internal/effect/circular.js +12 -10
- package/dist/cjs/internal/effect/circular.js.map +1 -1
- package/dist/cjs/internal/rateLimiter.js +39 -8
- package/dist/cjs/internal/rateLimiter.js.map +1 -1
- package/dist/cjs/internal/version.js +1 -1
- package/dist/dts/Effect.d.ts +6 -2
- package/dist/dts/Effect.d.ts.map +1 -1
- package/dist/dts/RateLimiter.d.ts +109 -10
- package/dist/dts/RateLimiter.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -5
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/version.d.ts +1 -1
- package/dist/esm/Effect.js.map +1 -1
- package/dist/esm/RateLimiter.js +66 -0
- package/dist/esm/RateLimiter.js.map +1 -1
- package/dist/esm/index.js +1 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/effect/circular.js +12 -10
- package/dist/esm/internal/effect/circular.js.map +1 -1
- package/dist/esm/internal/rateLimiter.js +36 -6
- package/dist/esm/internal/rateLimiter.js.map +1 -1
- package/dist/esm/internal/version.js +1 -1
- package/package.json +1 -1
- package/src/Effect.ts +6 -2
- package/src/RateLimiter.ts +111 -14
- package/src/index.ts +1 -5
- package/src/internal/effect/circular.ts +16 -14
- package/src/internal/rateLimiter.ts +81 -19
- package/src/internal/version.ts +1 -1
|
@@ -1,12 +1,42 @@
|
|
|
1
|
+
import * as Duration from "../Duration.js";
|
|
1
2
|
import * as Effect from "../Effect.js";
|
|
2
|
-
import * as
|
|
3
|
+
import * as FiberRef from "../FiberRef.js";
|
|
4
|
+
import { globalValue } from "../GlobalValue.js";
|
|
3
5
|
/** @internal */
|
|
4
|
-
export const make = (
|
|
5
|
-
|
|
6
|
+
export const make = ({
|
|
7
|
+
algorithm = "token-bucket",
|
|
8
|
+
interval,
|
|
9
|
+
limit
|
|
10
|
+
}) => {
|
|
11
|
+
switch (algorithm) {
|
|
12
|
+
case "fixed-window":
|
|
13
|
+
{
|
|
14
|
+
return fixedWindow(limit, interval);
|
|
15
|
+
}
|
|
16
|
+
case "token-bucket":
|
|
17
|
+
{
|
|
18
|
+
return tokenBucket(limit, interval);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const tokenBucket = (limit, window) => Effect.gen(function* (_) {
|
|
23
|
+
const millisPerToken = Math.ceil(Duration.toMillis(window) / limit);
|
|
6
24
|
const semaphore = yield* _(Effect.makeSemaphore(limit));
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
25
|
+
const latch = yield* _(Effect.makeSemaphore(0));
|
|
26
|
+
const refill = Effect.sleep(millisPerToken).pipe(Effect.zipRight(latch.releaseAll), Effect.zipRight(semaphore.release(1)), Effect.flatMap(free => free === limit ? Effect.unit : refill));
|
|
27
|
+
yield* _(latch.take(1), Effect.zipRight(refill), Effect.forever, Effect.forkScoped, Effect.interruptible);
|
|
28
|
+
const take = Effect.uninterruptibleMask(restore => Effect.flatMap(FiberRef.get(currentCost), cost => Effect.zipRight(restore(semaphore.take(cost)), latch.release(1))));
|
|
10
29
|
return effect => Effect.zipRight(take, effect);
|
|
11
30
|
});
|
|
31
|
+
const fixedWindow = (limit, window) => Effect.gen(function* (_) {
|
|
32
|
+
const semaphore = yield* _(Effect.makeSemaphore(limit));
|
|
33
|
+
const latch = yield* _(Effect.makeSemaphore(0));
|
|
34
|
+
yield* _(latch.take(1), Effect.zipRight(Effect.sleep(window)), Effect.zipRight(latch.releaseAll), Effect.zipRight(semaphore.releaseAll), Effect.forever, Effect.forkScoped, Effect.interruptible);
|
|
35
|
+
const take = Effect.uninterruptibleMask(restore => Effect.flatMap(FiberRef.get(currentCost), cost => Effect.zipRight(restore(semaphore.take(cost)), latch.release(1))));
|
|
36
|
+
return effect => Effect.zipRight(take, effect);
|
|
37
|
+
});
|
|
38
|
+
/** @internal */
|
|
39
|
+
const currentCost = /*#__PURE__*/globalValue( /*#__PURE__*/Symbol.for("effect/RateLimiter/currentCost"), () => FiberRef.unsafeMake(1));
|
|
40
|
+
/** @internal */
|
|
41
|
+
export const withCost = cost => Effect.locally(currentCost, cost);
|
|
12
42
|
//# sourceMappingURL=rateLimiter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimiter.js","names":["Effect","
|
|
1
|
+
{"version":3,"file":"rateLimiter.js","names":["Duration","Effect","FiberRef","globalValue","make","algorithm","interval","limit","fixedWindow","tokenBucket","window","gen","_","millisPerToken","Math","ceil","toMillis","semaphore","makeSemaphore","latch","refill","sleep","pipe","zipRight","releaseAll","release","flatMap","free","unit","take","forever","forkScoped","interruptible","uninterruptibleMask","restore","get","currentCost","cost","effect","Symbol","for","unsafeMake","withCost","locally"],"sources":["../../../src/internal/rateLimiter.ts"],"sourcesContent":[null],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const moduleVersion = "2.3.
|
|
1
|
+
export const moduleVersion = "2.3.4";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
package/src/Effect.ts
CHANGED
|
@@ -4673,10 +4673,14 @@ export interface Permit {
|
|
|
4673
4673
|
* @since 2.0.0
|
|
4674
4674
|
*/
|
|
4675
4675
|
export interface Semaphore {
|
|
4676
|
+
/** when the given amount of permits are available, run the effect and release the permits when finished */
|
|
4676
4677
|
withPermits(permits: number): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, R>
|
|
4678
|
+
/** take the given amount of permits, suspending if they are not yet available */
|
|
4677
4679
|
take(permits: number): Effect<number>
|
|
4678
|
-
release
|
|
4679
|
-
|
|
4680
|
+
/** release the given amount of permits, and return the resulting available permits */
|
|
4681
|
+
release(permits: number): Effect<number>
|
|
4682
|
+
/** release all the taken permits, and return the resulting available permits */
|
|
4683
|
+
releaseAll: Effect<number>
|
|
4680
4684
|
}
|
|
4681
4685
|
|
|
4682
4686
|
/**
|
package/src/RateLimiter.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Limits the number of calls to a resource to a maximum amount in some interval
|
|
3
|
-
*
|
|
4
|
-
* Note that only the moment of starting the effect is rate limited: the number of concurrent executions is not bounded.
|
|
5
|
-
*
|
|
6
|
-
* Calls are queued up in an unbounded queue until capacity becomes available.
|
|
2
|
+
* Limits the number of calls to a resource to a maximum amount in some interval.
|
|
7
3
|
*
|
|
8
4
|
* @since 2.0.0
|
|
9
5
|
*/
|
|
@@ -13,11 +9,10 @@ import * as internal from "./internal/rateLimiter.js"
|
|
|
13
9
|
import type { Scope } from "./Scope.js"
|
|
14
10
|
|
|
15
11
|
/**
|
|
16
|
-
* Limits the number of calls to a resource to a maximum amount in some interval
|
|
17
|
-
*
|
|
18
|
-
* Note that only the moment of starting the effect is rate limited: the number of concurrent executions is not bounded.
|
|
12
|
+
* Limits the number of calls to a resource to a maximum amount in some interval.
|
|
19
13
|
*
|
|
20
|
-
*
|
|
14
|
+
* Note that only the moment of starting the effect is rate limited: the number
|
|
15
|
+
* of concurrent executions is not bounded.
|
|
21
16
|
*
|
|
22
17
|
* @since 2.0.0
|
|
23
18
|
* @category models
|
|
@@ -27,11 +22,113 @@ export interface RateLimiter {
|
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
/**
|
|
25
|
+
* @since 2.0.0
|
|
26
|
+
*/
|
|
27
|
+
export declare namespace RateLimiter {
|
|
28
|
+
/**
|
|
29
|
+
* @since 2.0.0
|
|
30
|
+
* @category models
|
|
31
|
+
*/
|
|
32
|
+
export interface Options {
|
|
33
|
+
/**
|
|
34
|
+
* The maximum number of requests that should be allowed.
|
|
35
|
+
*/
|
|
36
|
+
readonly limit: number
|
|
37
|
+
/**
|
|
38
|
+
* The interval to utilize for rate-limiting requests. The semantics of the
|
|
39
|
+
* specified `interval` vary depending on the chosen `algorithm`:
|
|
40
|
+
*
|
|
41
|
+
* `token-bucket`: The maximum number of requests will be spread out over
|
|
42
|
+
* the provided interval if no tokens are available.
|
|
43
|
+
*
|
|
44
|
+
* For example, for a `RateLimiter` using the `token-bucket` algorithm with
|
|
45
|
+
* a `limit` of `10` and an `interval` of `1 seconds`, `1` request can be
|
|
46
|
+
* made every `100 millis`.
|
|
47
|
+
*
|
|
48
|
+
* `fixed-window`: The maximum number of requests will be reset during each
|
|
49
|
+
* interval. For example, for a `RateLimiter` using the `fixed-window`
|
|
50
|
+
* algorithm with a `limit` of `10` and an `interval` of `1 seconds`, a
|
|
51
|
+
* maximum of `10` requests can be made each second.
|
|
52
|
+
*/
|
|
53
|
+
readonly interval: DurationInput
|
|
54
|
+
/**
|
|
55
|
+
* The algorithm to utilize for rate-limiting requests.
|
|
56
|
+
*
|
|
57
|
+
* Defaults to `token-bucket`.
|
|
58
|
+
*/
|
|
59
|
+
readonly algorithm?: "fixed-window" | "token-bucket"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Constructs a new `RateLimiter` which will utilize the specified algorithm
|
|
65
|
+
* to limit requests (defaults to `token-bucket`).
|
|
66
|
+
*
|
|
67
|
+
* Notes
|
|
68
|
+
* - Only the moment of starting the effect is rate limited. The number of concurrent executions is not bounded.
|
|
69
|
+
* - Instances of `RateLimiter` can be composed.
|
|
70
|
+
* - The "cost" per effect can be changed. See {@link withCost}
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* import { Effect, RateLimiter } from "effect";
|
|
74
|
+
* import { compose } from "effect/Function"
|
|
75
|
+
*
|
|
76
|
+
* const program = Effect.scoped(
|
|
77
|
+
* Effect.gen(function* ($) {
|
|
78
|
+
* const perMinuteRL = yield* $(RateLimiter.make({ limit: 30, interval: "1 minutes" }))
|
|
79
|
+
* const perSecondRL = yield* $(RateLimiter.make({ limit: 2, interval: "1 seconds" }))
|
|
80
|
+
*
|
|
81
|
+
* // This rate limiter respects both the 30 calls per minute
|
|
82
|
+
* // and the 2 calls per second constraints.
|
|
83
|
+
* const rateLimit = compose(perMinuteRL, perSecondRL)
|
|
84
|
+
*
|
|
85
|
+
* // simulate repeated calls
|
|
86
|
+
* for (let n = 0; n < 100; n++) {
|
|
87
|
+
* // wrap the effect we want to limit with rateLimit
|
|
88
|
+
* yield* $(rateLimit(Effect.log("Calling RateLimited Effect")));
|
|
89
|
+
* }
|
|
90
|
+
* })
|
|
91
|
+
* );
|
|
92
|
+
*
|
|
30
93
|
* @since 2.0.0
|
|
31
94
|
* @category constructors
|
|
32
95
|
*/
|
|
33
|
-
export const make: (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
96
|
+
export const make: (options: RateLimiter.Options) => Effect<RateLimiter, never, Scope> = internal.make
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Alters the per-effect cost of the rate-limiter.
|
|
100
|
+
*
|
|
101
|
+
* This can be used for "credit" based rate-limiting where different API endpoints
|
|
102
|
+
* cost a different number of credits within a time window.
|
|
103
|
+
* Eg: 1000 credits / hour, where a query costs 1 credit and a mutation costs 5 credits.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* import { Effect, RateLimiter } from "effect";
|
|
107
|
+
* import { compose } from "effect/Function";
|
|
108
|
+
*
|
|
109
|
+
* const program = Effect.scoped(
|
|
110
|
+
* Effect.gen(function* ($) {
|
|
111
|
+
* // Create a rate limiter that has an hourly limit of 1000 credits
|
|
112
|
+
* const rateLimiter = yield* $(RateLimiter.make({ limit: 1000, interval: "1 hours" }));
|
|
113
|
+
* // Query API costs 1 credit per call ( 1 is the default cost )
|
|
114
|
+
* const queryAPIRL = compose(rateLimiter, RateLimiter.withCost(1));
|
|
115
|
+
* // Mutation API costs 5 credits per call
|
|
116
|
+
* const mutationAPIRL = compose(rateLimiter, RateLimiter.withCost(5));
|
|
117
|
+
|
|
118
|
+
* // Use the pre-defined rate limiters
|
|
119
|
+
* yield* $(queryAPIRL(Effect.log("Sample Query")));
|
|
120
|
+
* yield* $(mutationAPIRL(Effect.log("Sample Mutation")));
|
|
121
|
+
*
|
|
122
|
+
* // Or set a cost on-the-fly
|
|
123
|
+
* yield* $(
|
|
124
|
+
* rateLimiter(Effect.log("Another query with a different cost")).pipe(
|
|
125
|
+
* RateLimiter.withCost(3)
|
|
126
|
+
* )
|
|
127
|
+
* );
|
|
128
|
+
* })
|
|
129
|
+
* );
|
|
130
|
+
*
|
|
131
|
+
* @since 2.0.0
|
|
132
|
+
* @category combinators
|
|
133
|
+
*/
|
|
134
|
+
export const withCost: (cost: number) => <A, E, R>(effect: Effect<A, E, R>) => Effect<A, E, R> = internal.withCost
|
package/src/index.ts
CHANGED
|
@@ -545,11 +545,7 @@ export * as Queue from "./Queue.js"
|
|
|
545
545
|
export * as Random from "./Random.js"
|
|
546
546
|
|
|
547
547
|
/**
|
|
548
|
-
* Limits the number of calls to a resource to a maximum amount in some interval
|
|
549
|
-
*
|
|
550
|
-
* Note that only the moment of starting the effect is rate limited: the number of concurrent executions is not bounded.
|
|
551
|
-
*
|
|
552
|
-
* Calls are queued up in an unbounded queue until capacity becomes available.
|
|
548
|
+
* Limits the number of calls to a resource to a maximum amount in some interval.
|
|
553
549
|
*
|
|
554
550
|
* @since 2.0.0
|
|
555
551
|
*/
|
|
@@ -35,7 +35,7 @@ import * as supervisor from "../supervisor.js"
|
|
|
35
35
|
|
|
36
36
|
/** @internal */
|
|
37
37
|
class Semaphore {
|
|
38
|
-
public waiters = new Set<() =>
|
|
38
|
+
public waiters = new Set<() => void>()
|
|
39
39
|
public taken = 0
|
|
40
40
|
|
|
41
41
|
constructor(readonly permits: number) {}
|
|
@@ -49,12 +49,11 @@ class Semaphore {
|
|
|
49
49
|
if (this.free < n) {
|
|
50
50
|
const observer = () => {
|
|
51
51
|
if (this.free < n) {
|
|
52
|
-
return
|
|
52
|
+
return
|
|
53
53
|
}
|
|
54
54
|
this.waiters.delete(observer)
|
|
55
55
|
this.taken += n
|
|
56
56
|
resume(core.succeed(n))
|
|
57
|
-
return true
|
|
58
57
|
}
|
|
59
58
|
this.waiters.add(observer)
|
|
60
59
|
return Either.left(core.sync(() => {
|
|
@@ -65,22 +64,25 @@ class Semaphore {
|
|
|
65
64
|
return Either.right(core.succeed(n))
|
|
66
65
|
})
|
|
67
66
|
|
|
68
|
-
readonly updateTaken = (f: (n: number) => number): Effect.Effect<
|
|
67
|
+
readonly updateTaken = (f: (n: number) => number): Effect.Effect<number> =>
|
|
69
68
|
core.withFiberRuntime((fiber) => {
|
|
70
69
|
this.taken = f(this.taken)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
item
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
if (this.waiters.size > 0) {
|
|
71
|
+
fiber.getFiberRef(currentScheduler).scheduleTask(() => {
|
|
72
|
+
const iter = this.waiters.values()
|
|
73
|
+
let item = iter.next()
|
|
74
|
+
while (item.done === false && this.free > 0) {
|
|
75
|
+
item.value()
|
|
76
|
+
item = iter.next()
|
|
77
|
+
}
|
|
78
|
+
}, fiber.getFiberRef(core.currentSchedulingPriority))
|
|
79
|
+
}
|
|
80
|
+
return core.succeed(this.free)
|
|
79
81
|
})
|
|
80
82
|
|
|
81
|
-
readonly release = (n: number): Effect.Effect<
|
|
83
|
+
readonly release = (n: number): Effect.Effect<number> => this.updateTaken((taken) => taken - n)
|
|
82
84
|
|
|
83
|
-
readonly releaseAll: Effect.Effect<
|
|
85
|
+
readonly releaseAll: Effect.Effect<number> = this.updateTaken((_) => 0)
|
|
84
86
|
|
|
85
87
|
readonly withPermits = (n: number) => <R, E, A>(self: Effect.Effect<R, E, A>) =>
|
|
86
88
|
core.uninterruptibleMask((restore) =>
|
|
@@ -1,30 +1,92 @@
|
|
|
1
1
|
import type { DurationInput } from "../Duration.js"
|
|
2
|
+
import * as Duration from "../Duration.js"
|
|
2
3
|
import * as Effect from "../Effect.js"
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import * as
|
|
4
|
+
import * as FiberRef from "../FiberRef.js"
|
|
5
|
+
import { globalValue } from "../GlobalValue.js"
|
|
6
|
+
import type * as RateLimiter from "../RateLimiter.js"
|
|
7
|
+
import type * as Scope from "../Scope.js"
|
|
6
8
|
|
|
7
9
|
/** @internal */
|
|
8
|
-
export const make = (
|
|
9
|
-
|
|
10
|
+
export const make = ({
|
|
11
|
+
algorithm = "token-bucket",
|
|
12
|
+
interval,
|
|
13
|
+
limit
|
|
14
|
+
}: RateLimiter.RateLimiter.Options): Effect.Effect<
|
|
15
|
+
RateLimiter.RateLimiter,
|
|
10
16
|
never,
|
|
11
|
-
Scope
|
|
17
|
+
Scope.Scope
|
|
18
|
+
> => {
|
|
19
|
+
switch (algorithm) {
|
|
20
|
+
case "fixed-window": {
|
|
21
|
+
return fixedWindow(limit, interval)
|
|
22
|
+
}
|
|
23
|
+
case "token-bucket": {
|
|
24
|
+
return tokenBucket(limit, interval)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tokenBucket = (limit: number, window: DurationInput): Effect.Effect<
|
|
30
|
+
RateLimiter.RateLimiter,
|
|
31
|
+
never,
|
|
32
|
+
Scope.Scope
|
|
12
33
|
> =>
|
|
13
34
|
Effect.gen(function*(_) {
|
|
14
|
-
const
|
|
35
|
+
const millisPerToken = Math.ceil(Duration.toMillis(window) / limit)
|
|
15
36
|
const semaphore = yield* _(Effect.makeSemaphore(limit))
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
const latch = yield* _(Effect.makeSemaphore(0))
|
|
38
|
+
const refill: Effect.Effect<void> = Effect.sleep(millisPerToken).pipe(
|
|
39
|
+
Effect.zipRight(latch.releaseAll),
|
|
40
|
+
Effect.zipRight(semaphore.release(1)),
|
|
41
|
+
Effect.flatMap((free) => free === limit ? Effect.unit : refill)
|
|
42
|
+
)
|
|
43
|
+
yield* _(
|
|
44
|
+
latch.take(1),
|
|
45
|
+
Effect.zipRight(refill),
|
|
46
|
+
Effect.forever,
|
|
47
|
+
Effect.forkScoped,
|
|
48
|
+
Effect.interruptible
|
|
49
|
+
)
|
|
50
|
+
const take = Effect.uninterruptibleMask((restore) =>
|
|
51
|
+
Effect.flatMap(
|
|
52
|
+
FiberRef.get(currentCost),
|
|
53
|
+
(cost) => Effect.zipRight(restore(semaphore.take(cost)), latch.release(1))
|
|
54
|
+
)
|
|
27
55
|
)
|
|
28
|
-
const take = Effect.zipRight(semaphore.take(1), reset)
|
|
29
56
|
return (effect) => Effect.zipRight(take, effect)
|
|
30
57
|
})
|
|
58
|
+
|
|
59
|
+
const fixedWindow = (limit: number, window: DurationInput): Effect.Effect<
|
|
60
|
+
RateLimiter.RateLimiter,
|
|
61
|
+
never,
|
|
62
|
+
Scope.Scope
|
|
63
|
+
> =>
|
|
64
|
+
Effect.gen(function*(_) {
|
|
65
|
+
const semaphore = yield* _(Effect.makeSemaphore(limit))
|
|
66
|
+
const latch = yield* _(Effect.makeSemaphore(0))
|
|
67
|
+
yield* _(
|
|
68
|
+
latch.take(1),
|
|
69
|
+
Effect.zipRight(Effect.sleep(window)),
|
|
70
|
+
Effect.zipRight(latch.releaseAll),
|
|
71
|
+
Effect.zipRight(semaphore.releaseAll),
|
|
72
|
+
Effect.forever,
|
|
73
|
+
Effect.forkScoped,
|
|
74
|
+
Effect.interruptible
|
|
75
|
+
)
|
|
76
|
+
const take = Effect.uninterruptibleMask((restore) =>
|
|
77
|
+
Effect.flatMap(
|
|
78
|
+
FiberRef.get(currentCost),
|
|
79
|
+
(cost) => Effect.zipRight(restore(semaphore.take(cost)), latch.release(1))
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return (effect) => Effect.zipRight(take, effect)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/** @internal */
|
|
86
|
+
const currentCost = globalValue(
|
|
87
|
+
Symbol.for("effect/RateLimiter/currentCost"),
|
|
88
|
+
() => FiberRef.unsafeMake(1)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
/** @internal */
|
|
92
|
+
export const withCost = (cost: number) => Effect.locally(currentCost, cost)
|
package/src/internal/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const moduleVersion = "2.3.
|
|
1
|
+
export const moduleVersion = "2.3.4"
|