Skip to content

Commit 21a4165

Browse files
committed
Initial commit
0 parents  commit 21a4165

14 files changed

Lines changed: 620 additions & 0 deletions

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.buildlog/
9+
.history
10+
.svn/
11+
migrate_working_dir/
12+
13+
# IntelliJ related
14+
*.iml
15+
*.ipr
16+
*.iws
17+
.idea/
18+
19+
# The .vscode folder contains launch configuration and tasks you configure in
20+
# VS Code which you may wish to be included in version control, so this line
21+
# is commented out by default.
22+
#.vscode/
23+
24+
# Flutter/Dart/Pub related
25+
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26+
/pubspec.lock
27+
**/doc/api/
28+
.dart_tool/
29+
build/

.metadata

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: "b0850beeb25f6d5b10426284f506557f66181b36"
8+
channel: "stable"
9+
10+
project_type: package

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.1.0
2+
3+
- initial release

LICENSE

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2024, Martin Klüpfel
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!--
2+
This README describes the package. If you publish this package to pub.dev,
3+
this README's contents appear on the landing page for your package.
4+
5+
For information about how to write a good package README, see the guide for
6+
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
7+
8+
For general information about developing packages, see the Dart guide for
9+
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
10+
and the Flutter guide for
11+
[developing packages and plugins](https://flutter.dev/developing-packages).
12+
-->
13+
14+
TODO: Put a short description of the package here that helps potential users
15+
know whether this package might be useful for them.
16+
17+
## Features
18+
19+
TODO: List what your package can do. Maybe include images, gifs, or videos.
20+
21+
## Getting started
22+
23+
TODO: List prerequisites and provide or point to information on how to
24+
start using the package.
25+
26+
## Usage
27+
28+
TODO: Include short and useful examples for package users. Add longer examples
29+
to `/example` folder.
30+
31+
```dart
32+
const like = 'sample';
33+
```
34+
35+
## Additional information
36+
37+
TODO: Tell users more about the package: where to find more information, how to
38+
contribute to the package, how to file issues, what response they can expect
39+
from the package authors, and more.

analysis_options.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include: package:flutter_lints/flutter.yaml
2+
3+
# Additional information about this file can be found at
4+
# https://dart.dev/guides/language/analysis-options

coverage/lcov.info

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
SF:lib/src/token_bucket_state.dart
2+
DA:5,1
3+
DA:7,1
4+
DA:8,1
5+
DA:9,1
6+
DA:10,1
7+
DA:14,1
8+
DA:15,5
9+
DA:17,1
10+
DA:18,1
11+
DA:19,1
12+
DA:21,2
13+
DA:24,2
14+
DA:25,1
15+
DA:26,2
16+
DA:29,1
17+
DA:32,1
18+
DA:33,3
19+
DA:34,3
20+
DA:35,3
21+
DA:37,1
22+
DA:38,5
23+
LF:21
24+
LH:21
25+
end_of_record
26+
SF:lib/src/token_bucket.dart
27+
DA:14,1
28+
DA:19,2
29+
DA:20,2
30+
DA:21,2
31+
DA:27,1
32+
DA:28,2
33+
DA:31,2
34+
DA:32,1
35+
DA:35,4
36+
DA:36,2
37+
DA:38,6
38+
DA:40,1
39+
DA:42,4
40+
DA:50,1
41+
DA:55,2
42+
DA:57,1
43+
DA:59,2
44+
DA:60,5
45+
DA:63,6
46+
DA:66,1
47+
DA:68,3
48+
DA:69,3
49+
DA:71,2
50+
DA:72,3
51+
DA:73,1
52+
DA:74,2
53+
DA:79,1
54+
DA:80,4
55+
DA:81,3
56+
DA:82,2
57+
DA:89,1
58+
DA:94,2
59+
DA:96,1
60+
DA:98,5
61+
DA:99,3
62+
DA:102,1
63+
DA:104,3
64+
DA:105,3
65+
DA:107,3
66+
DA:108,1
67+
DA:109,2
68+
LF:41
69+
LH:41
70+
end_of_record
71+
SF:lib/src/token_bucket_storage.dart
72+
DA:7,1
73+
DA:17,1
74+
DA:22,1
75+
DA:23,1
76+
DA:25,1
77+
DA:26,1
78+
DA:30,1
79+
LF:7
80+
LH:7
81+
end_of_record

lib/src/token_bucket.dart

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import 'dart:async';
2+
import 'dart:math';
3+
4+
import 'package:clock/clock.dart';
5+
import 'package:token_bucket_algorithm/src/token_bucket_storage.dart';
6+
7+
part 'token_bucket_state.dart';
8+
9+
abstract class _BaseTokenBucket<S extends AsyncTokenBucketStorage> {
10+
/// The maximum token amount of this bucket.
11+
final int size;
12+
13+
/// The interval for refilling the bucket.
14+
final Duration refillInterval;
15+
16+
/// The amount of tokens that is refilled every [refillInterval].
17+
final int refillAmount;
18+
19+
/// The storage for the internal [TokenBucketState].
20+
final S storage;
21+
22+
_BaseTokenBucket({
23+
required this.size,
24+
required this.refillInterval,
25+
required this.refillAmount,
26+
required this.storage,
27+
}) : assert(size > 0),
28+
assert(refillAmount > 0),
29+
assert(refillInterval > Duration.zero);
30+
31+
/// Returns the currently available tokens of this bucket.
32+
FutureOr<int> get availableTokens;
33+
34+
/// Consumes [cost] tokens and returns whether the consuming was successful.
35+
FutureOr<bool> consume([int cost = 1]);
36+
37+
TokenBucketState _refillBucket(TokenBucketState state) {
38+
final now = clock.now();
39+
40+
// relevant if e.g. user updates time in settings
41+
if (state.lastRefillTime.isAfter(now)) {
42+
return state.copyWith(lastRefillTime: now);
43+
}
44+
45+
final refillTimes = (now.difference(state.lastRefillTime)).inMicroseconds ~/
46+
refillInterval.inMicroseconds;
47+
48+
final newTokenCount = min(size, state.tokens + refillTimes * refillAmount);
49+
50+
return state.copyWith(
51+
tokens: newTokenCount,
52+
lastRefillTime: state.lastRefillTime.add(refillInterval * refillTimes),
53+
);
54+
}
55+
}
56+
57+
/// An async token bucket which can have an async [storage].
58+
class AsyncTokenBucket extends _BaseTokenBucket<AsyncTokenBucketStorage> {
59+
Future<void> _future = Future.value();
60+
61+
AsyncTokenBucket({
62+
required super.size,
63+
required super.refillInterval,
64+
required super.refillAmount,
65+
AsyncTokenBucketStorage? storage,
66+
}) : super(storage: storage ?? MemoryTokenBucketStorage());
67+
68+
@override
69+
FutureOr<int> get availableTokens async {
70+
await _queueFuture(() async {
71+
await storage.set(_refillBucket(await storage.get()));
72+
return false;
73+
});
74+
return Future.value(storage.get()).then((state) => state.tokens);
75+
}
76+
77+
@override
78+
FutureOr<bool> consume([int cost = 1]) async {
79+
if (cost < 1 || cost > size) {
80+
throw ArgumentError('cost must be <=$size and >=1');
81+
}
82+
return _queueFuture(() async {
83+
final state = _refillBucket(await storage.get());
84+
final (result, newState) = state.consume(cost);
85+
await storage.set(newState);
86+
return result;
87+
});
88+
}
89+
90+
Future<T> _queueFuture<T>(Future<T> Function() computation) {
91+
final newFuture = _future.then((_) => computation());
92+
_future = newFuture.whenComplete(() {
93+
if (_future == newFuture) _future = Future.value();
94+
});
95+
return newFuture;
96+
}
97+
}
98+
99+
/// A standard token bucket.
100+
///
101+
/// If you want to store the [TokenBucketState] in an [AsyncTokenBucketStorage],
102+
/// you have to use [AsyncTokenBucket] instead.
103+
class TokenBucket extends _BaseTokenBucket<TokenBucketStorage> {
104+
TokenBucket({
105+
required super.size,
106+
required super.refillInterval,
107+
required super.refillAmount,
108+
TokenBucketStorage? storage,
109+
}) : super(storage: storage ?? MemoryTokenBucketStorage());
110+
111+
@override
112+
int get availableTokens {
113+
storage.set(_refillBucket(storage.get()));
114+
return storage.get().tokens;
115+
}
116+
117+
@override
118+
bool consume([int cost = 1]) {
119+
if (cost < 1 || cost > size) {
120+
throw ArgumentError('cost must be <=$size and >=1');
121+
}
122+
final state = _refillBucket(storage.get());
123+
final (result, newState) = state.consume(cost);
124+
storage.set(newState);
125+
return result;
126+
}
127+
}

lib/src/token_bucket_state.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
part of 'token_bucket.dart';
2+
3+
class TokenBucketState {
4+
/// Currently available tokens.
5+
final int tokens;
6+
7+
/// The last time the tokens were refilled.
8+
final DateTime lastRefillTime;
9+
10+
const TokenBucketState({required this.tokens, required this.lastRefillTime});
11+
12+
/// Copies this state with [tokens] or [lastRefillTime] updated.
13+
TokenBucketState copyWith({int? tokens, DateTime? lastRefillTime}) {
14+
return TokenBucketState(
15+
tokens: tokens ?? this.tokens,
16+
lastRefillTime: lastRefillTime ?? this.lastRefillTime,
17+
);
18+
}
19+
20+
(bool, TokenBucketState) consume(int cost) =>
21+
tokens < cost ? (false, this) : (true, copyWith(tokens: tokens - cost));
22+
23+
factory TokenBucketState.fromJson(Map<String, dynamic> json) =>
24+
TokenBucketState(
25+
tokens: json['tokens'] as int,
26+
lastRefillTime:
27+
DateTime.fromMicrosecondsSinceEpoch(json['lastRefillTime'] as int),
28+
);
29+
30+
Map<String, dynamic> toJson() => {
31+
'tokens': tokens,
32+
'lastRefillTime': lastRefillTime.microsecondsSinceEpoch,
33+
};
34+
35+
@override
36+
bool operator ==(Object other) =>
37+
identical(this, other) ||
38+
other is TokenBucketState &&
39+
runtimeType == other.runtimeType &&
40+
tokens == other.tokens &&
41+
lastRefillTime == other.lastRefillTime;
42+
43+
@override
44+
int get hashCode => tokens.hashCode ^ lastRefillTime.hashCode;
45+
}

0 commit comments

Comments
 (0)