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.
@@ -1,12 +1,42 @@
1
+ import * as Duration from "../Duration.js";
1
2
  import * as Effect from "../Effect.js";
2
- import * as SynchronizedRef from "../SynchronizedRef.js";
3
+ import * as FiberRef from "../FiberRef.js";
4
+ import { globalValue } from "../GlobalValue.js";
3
5
  /** @internal */
4
- export const make = (limit, window) => Effect.gen(function* (_) {
5
- const scope = yield* _(Effect.scope);
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 ref = yield* _(SynchronizedRef.make(false));
8
- const reset = SynchronizedRef.updateEffect(ref, running => running ? Effect.succeed(true) : Effect.sleep(window).pipe(Effect.zipRight(SynchronizedRef.set(ref, false)), Effect.zipRight(semaphore.releaseAll), Effect.forkIn(scope), Effect.interruptible, Effect.as(true)));
9
- const take = Effect.zipRight(semaphore.take(1), reset);
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","SynchronizedRef","make","limit","window","gen","_","scope","semaphore","makeSemaphore","ref","reset","updateEffect","running","succeed","sleep","pipe","zipRight","set","releaseAll","forkIn","interruptible","as","take","effect"],"sources":["../../../src/internal/rateLimiter.ts"],"sourcesContent":[null],"mappings":"AACA,OAAO,KAAKA,MAAM,MAAM,cAAc;AAGtC,OAAO,KAAKC,eAAe,MAAM,uBAAuB;AAExD;AACA,OAAO,MAAMC,IAAI,GAAGA,CAACC,KAAa,EAAEC,MAAqB,KAKvDJ,MAAM,CAACK,GAAG,CAAC,WAAUC,CAAC;EACpB,MAAMC,KAAK,GAAG,OAAOD,CAAC,CAACN,MAAM,CAACO,KAAK,CAAC;EACpC,MAAMC,SAAS,GAAG,OAAOF,CAAC,CAACN,MAAM,CAACS,aAAa,CAACN,KAAK,CAAC,CAAC;EACvD,MAAMO,GAAG,GAAG,OAAOJ,CAAC,CAACL,eAAe,CAACC,IAAI,CAAC,KAAK,CAAC,CAAC;EACjD,MAAMS,KAAK,GAAGV,eAAe,CAACW,YAAY,CACxCF,GAAG,EACFG,OAAO,IACNA,OAAO,GAAGb,MAAM,CAACc,OAAO,CAAC,IAAI,CAAC,GAAGd,MAAM,CAACe,KAAK,CAACX,MAAM,CAAC,CAACY,IAAI,CACxDhB,MAAM,CAACiB,QAAQ,CAAChB,eAAe,CAACiB,GAAG,CAACR,GAAG,EAAE,KAAK,CAAC,CAAC,EAChDV,MAAM,CAACiB,QAAQ,CAACT,SAAS,CAACW,UAAU,CAAC,EACrCnB,MAAM,CAACoB,MAAM,CAACb,KAAK,CAAC,EACpBP,MAAM,CAACqB,aAAa,EACpBrB,MAAM,CAACsB,EAAE,CAAC,IAAI,CAAC,CAChB,CACJ;EACD,MAAMC,IAAI,GAAGvB,MAAM,CAACiB,QAAQ,CAACT,SAAS,CAACe,IAAI,CAAC,CAAC,CAAC,EAAEZ,KAAK,CAAC;EACtD,OAAQa,MAAM,IAAKxB,MAAM,CAACiB,QAAQ,CAACM,IAAI,EAAEC,MAAM,CAAC;AAClD,CAAC,CAAC"}
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.2";
1
+ export const moduleVersion = "2.3.4";
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "Functional programming in TypeScript",
5
5
  "license": "MIT",
6
6
  "repository": {
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(permits: number): Effect<void>
4679
- releaseAll: Effect<void>
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
  /**
@@ -1,9 +1,5 @@
1
1
  /**
2
- * Limits the number of calls to a resource to a maximum amount in some interval using the token bucket algorithm.
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 using the token bucket algorithm.
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
- * Calls are queued up in an unbounded queue until capacity becomes available.
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: (limit: number, window: DurationInput) => Effect<
34
- RateLimiter,
35
- never,
36
- Scope
37
- > = internal.make
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 using the token bucket algorithm.
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<() => boolean>()
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 false
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<void> =>
67
+ readonly updateTaken = (f: (n: number) => number): Effect.Effect<number> =>
69
68
  core.withFiberRuntime((fiber) => {
70
69
  this.taken = f(this.taken)
71
- fiber.getFiberRef(currentScheduler).scheduleTask(() => {
72
- const iter = this.waiters.values()
73
- let item = iter.next()
74
- while (item.done === false && item.value() === true) {
75
- item = iter.next()
76
- }
77
- }, fiber.getFiberRef(core.currentSchedulingPriority))
78
- return core.unit
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<void> => this.updateTaken((taken) => taken - n)
83
+ readonly release = (n: number): Effect.Effect<number> => this.updateTaken((taken) => taken - n)
82
84
 
83
- readonly releaseAll: Effect.Effect<void> = this.updateTaken((_) => 0)
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 type { RateLimiter } from "../RateLimiter.js"
4
- import type { Scope } from "../Scope.js"
5
- import * as SynchronizedRef from "../SynchronizedRef.js"
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 = (limit: number, window: DurationInput): Effect.Effect<
9
- RateLimiter,
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 scope = yield* _(Effect.scope)
35
+ const millisPerToken = Math.ceil(Duration.toMillis(window) / limit)
15
36
  const semaphore = yield* _(Effect.makeSemaphore(limit))
16
- const ref = yield* _(SynchronizedRef.make(false))
17
- const reset = SynchronizedRef.updateEffect(
18
- ref,
19
- (running) =>
20
- running ? Effect.succeed(true) : Effect.sleep(window).pipe(
21
- Effect.zipRight(SynchronizedRef.set(ref, false)),
22
- Effect.zipRight(semaphore.releaseAll),
23
- Effect.forkIn(scope),
24
- Effect.interruptible,
25
- Effect.as(true)
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)
@@ -1 +1 @@
1
- export const moduleVersion = "2.3.2"
1
+ export const moduleVersion = "2.3.4"