Algorithms and data structures in TypeScript: string tokenizer, rate limiter
Introduction #
This tutorial is part of a collection of tutorials on basic data structures and algorithms that are created using TypeScript. Information is depicted visually using diagrams and code snippets. This article may be useful if you are trying to get more fluency in TypeScript or need a refresher to do interview prep for software engineering roles.
This tutorial has 2 examples in it:
How to run this project #
You can get the code for this and all the other tutorials in this collection from this github repo. Once you clone this repo locally, you can run the following:
cd ts-string-tokenizer
npm install
npx jest --watchAll
All the examples in this repo are written in the form of tests and you can see them running in there. If you change the code while the tests are running, the tests will re-run automatically w/ these new changes. This is nice to have when you are learning how the code works.
Exercise 1: String tokenizer #
Create a function that mimics the
cd
command in the terminal.
Function signature #
function cd(currentPath: string, action: string): string
The function takes two argument, first argument is a current path and the second argument is an action to apply to the current path to change the path. The function modifies the path and returns the resulting path as a string.
Test against the following cases #
| Current path | Action | Resulting path |
| -------------------- | ------------------ | ------------------------------------ |
| / | /folder | /folder |
| /folder | nestedFolder | /folder/nestedFolder |
| /folder | /home | /home |
| /folder/nestedFolder | .. | /folder |
| /folder/nestedFolder | ../folder2/folder3 | /folder/folder2/folder3 |
| /folder/nestedFolder | folder2/./folder3 | /folder/nestedFolder/folder2/folder3 |
String tokenizer logic #
Our currentPath
is a string and we need to manipulate this string and eventually return a new
string.
Data modelling #
-
In order for us to manipulate a string easily, we chunk the string into smaller strings and store those smaller string chunks into an array.
- For example,
"/home/user/folder"
would be turned into an array of["home", "user", "folder"]
<- Data modelling. - We don’t want to store delimiters into our array, this would only complicate manipulating the array.
- We will add the delimiters back to our string later when we are returning output string.
- For example,
-
We have a string
currentPath
that we know we need to manipulate. But how? Our second argument is aaction
.action
is a string that tells us how to manipulate ourcurrentPath
.- The meaning of
action
string command: For exampleaction
can look like this:"../anotherFolder"
. This means we want to go up one folder (..) and then go into folder called anotherFolder. In order for us to understand what thisaction
string means, we need to break it down into smaller strings and store those smaller strings in an array as well. - For example,
"../anotherFolder"
would be turned into an array of["..", "anotherFolder"]
. - If we encounter
"."
, then ignore it and go to the next element in the array. - If the action starts with
"/"
then this means that whatever comes after the"/"
is the absolute path and the string output we want to return. - ❗IMPORTANT: This tells me that when we are chunking our
action
into a smaller strings, we need to think about how to treat the"/"
character, since we need to account for it (it tells us the new path is an absolute path). - 💡IDEA: We check if the first string character is
"/"
, then don’t chunk the string into an array and just return the string as output string, since it’s an absolute path.
- The meaning of
Assign meaning to each action #
-
Now we have
action
string array. In order to apply theaction
to ourcurrentPath
, we need to go through each array element, check what it means and then manipulate our currentPath accordingly. We need to define all of ouraction
cases:- If the first character is
"/"
, then we don’t chunk the string into an array and just return the string as output string, since it’s an absolute path. ".."
means go up one directory, which in terms or data structures means, remove the last element of ourcurrentPathArray
."anotherFolder"
means go into another folder, which in terms of data structures means, add the name of the anotherFolder to the end of ourcurrentPathArray
."."
means do nothing, which in terms of data structures means just go to the next element in the array.
- If the first character is
-
Chunking up
currentPath
andaction
strings to arrays:- FOR INTERVIEW I PREFER THIS: When we are chunking up our currentPath, we can use JS
split()
method and pass in a delimiter"/"
.- The caveat is that it returns an array of strings, but the first string is an empty string.
- If we choose to go with this option, we need to remove the first string from our array
using
shift()
(modifies the array, returns the removed element) orsplice(0,1)
(0 is start index, 1 means how many elements we need to remove from the start index. Splice modifies an array, returns removed element).
- If we choose to go with this option, we need to remove the first string from our array
using
- The caveat is that it returns an array of strings, but the first string is an empty string.
- Another way to chunk up our currentPath is to loop over it and start collecting characters into
a
currentString
variable, when encounter"/"
character, then push thecurrentString
tocurrentPathArray
and reset thecurrentString
to empty string.
- FOR INTERVIEW I PREFER THIS: When we are chunking up our currentPath, we can use JS
-
Manipulating
currentPathArray
:- Now that we have
cdActionArray
, we need to loop over it and for each element we check our use cases (see number 3) and manipulate ourcurrentPathArray
accordingly.
- Now that we have
-
Done!
Exercise 2: In Memory Rate Limiter #
Implement a in-memory rate limiter that limits the number of tokens that can be produced in a given time period.
Function signature #
function acceptOrDenyRequest(
maxRequests: number,
timeWindowInMs: number,
successfulRequests: number[],
newRequestTimestamp: number
): "accept" | "deny"
Test against the following cases #
| Request ## | Timestamp | Result |
| ---------- | --------- | ------ |
| 1 | 1100 | accept |
| 2 | 1200 | accept |
| 3 | 1300 | deny |
| 4 | 2100 | accept |
| 5 | 2150 | deny |
| 6 | 2200 | accept |
In Memory Rate Limiter Logic #
We have built and API and made it’s endpoint public. This means that in theory we could have millions of requests coming in to our API endpoint. This could be costly or we could have a malicious code that could potentially crash our server. We need to protect our API endpoint from malicious code and also from too many requests coming in at the same time. We need to implement a rate limiter that limits the number of tokens (requests) that can be produced in a given time period.
-
We are going to create two functions: outer and inner function. Outer function defines all the variables and data structures we need to keep track of and the inner function accepts all those variables as arguments and checks is there space left in the time window to allow another request in or not.
Things we need to keep track of in our outer function:
- We need to keep track of all the successful request timestamps. Create an empty array
successfulRequests
to start tracking the successful request timestamps. - We need to define how many requests we want to allow in a given time period. This is the
maxRequests
. - Then we need to define the time period that we count how many requests come in and which
requests to allow and which to deny. This is the
timeWindowInMs
. - One more thing. When a new request comes in, we need to create a timestamp for it. This is the
newRequestTimestamp
. This is the time window endpoint. We use this timestamp to calculate the window start point to check if there are less than max requests currently in the time window.
- We need to keep track of all the successful request timestamps. Create an empty array
-
Now we are going to call a function
acceptOrDenyRequest
which will accept or deny the request to hit the API endpoint. This function will take in the arguments we just defined:maxRequests: number, timeWindowInMs: number, successfulRequests: number[], newRequestTimestamp: number)
-
We need to keep track of request count in the time window. We create a
requestCount
variable. When the request count is equal to the max requests, we need to deny the request since the time window is full. -
We will loop through the
successfulRequests
array from back to front. We will check if the previously successful timestamp is still within the time window. If it is, we will increment the request count. If it is not, we will stop the loop since we know we have reached outside of the window bounds. -
Now that we know how many requests were successful in the current time window, we can compare successful requests number with the max requests allowed in the time window. If successful requests number is less than max request allowed, then we can accept the request. If successful requests number is equal to max requests allowed, then we need to deny the request since the time window is full with successful requests already.
-
-
In the outer function we check if the request was successful or not. It the request was successful, we push the request timestamp to
successfulRequests
array and return “accept”. If the request was not successful, we return “deny”.
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.
📦 Install our useful Rust command line apps usingcargo install r3bl-cmdr
(they are from the r3bl-open-core project):
- 🐱
giti
: run interactive git commands with confidence in your terminal- 🦜
edi
: edit Markdown with style in your terminalgiti in action
edi in action