Skip to content

Commit

Permalink
- Added a CHANGELOG.md to track the notable changes
Browse files Browse the repository at this point in the history
- Added a getTimestamp(...) argument to the 'throttle' and 'debounce' operators to allow using prerecorded timestamps
- Some small fixes to md files and comments to make typedoc happy
  • Loading branch information
mrft committed Aug 6, 2024
1 parent 1359ad6 commit de2d3f8
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 14 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Release Notes

cfr. [keepachangelog.com](https://keepachangelog.com/en/1.1.0/)

- `Added` for new features.
- `Changed` for changes in existing functionality.
- `Deprecated` for soon-to-be removed features.
- `Removed` for now removed features.
- `Fixed` for any bug fixes.
- `Security` in case of vulnerabilities.

## [Unreleased]

### Added

- a CHANGELOG.md file to track notable changes
- a getTimestamp(value) function argument to both the 'debounce' and the 'throttle' operators,
so the timestamps can be 'prerecorded' inside the values

### Changed

### Deprecated

### Removed

### Fixed

### Security

## version 0.4.7 (2024-07-13)

### Fixed
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ A response to the first issue above:

> The most useful thing to look at is for await. The behavior of for await is, if next returns a rejected promise, the loop will immediately stop, and will not call .return on the iterator (as it would if the loop early-exited any other way). In other words, for await treats next returning a rejected promise as indicating that the iterator is done.
>
> Also, when using an async generator, the only way for next to give a rejected promise is if the body of the generator throws an uncaught exception, in which case the generator is finished and any subsequent call to .next will resolve with { done: true }.
> Also, when using an async generator, the only way for next to give a rejected promise is if the body of the generator throws an uncaught exception, in which case the generator is finished and any subsequent call to .next will resolve with `{ done: true }`
>
> Now, of course, you can implement a different behavior if you want to. But if you want to match the behavior of the things in the language, "if the promise returned by .next() rejects then the iterator is finished" is the principle to follow, and I think that would be a reasonable thing to write down without getting into all of the edge cases around async-from-sync and so on.
Expand Down
34 changes: 32 additions & 2 deletions src/operators/timeBased/debounce.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { assert } from "chai";
import FakeTimers from "@sinonjs/fake-timers";
import { itr8Pushable, itr8ToArray, pipe } from "../../index.js";
import {
itr8FromIterable,
itr8Pushable,
itr8ToArray,
map,
pipe,
} from "../../index.js";
import { sleep } from "../../testUtils/index.js";
import { debounce } from "./debounce.js";

describe("operators/timeBased/debounce.ts", () => {
it("debounce(...) operator works properly", async () => {
it("debounce(...) operator without second argument works properly", async () => {
const clock = FakeTimers.install(); // don't forget to uninstall the clock in a finally block !
try {
const pushIt = itr8Pushable();
Expand Down Expand Up @@ -43,4 +49,28 @@ describe("operators/timeBased/debounce.ts", () => {
clock.uninstall();
}
});

it("debounce(...) operator with second argument works properly", () => {
const valueTimestampTuples = [
[1, 0],
[2, 10],
[3, 10],
[4, 40],
[5, 50],
[6, 60],
[7, 60],
[8, 60],
[9, 60],
[10, 100],
];

const result = pipe(
itr8FromIterable(valueTimestampTuples),
debounce(20, ([_v, ts]) => ts),
map(([v, _ts]) => v),
itr8ToArray,
);

assert.deepEqual(result, [1, 4, 10]);
});
});
43 changes: 40 additions & 3 deletions src/operators/timeBased/debounce.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
import { powerMap } from "../general/powerMap.js";

/**
* Only useful on async iterators.
* Mainly useful on async iterators (for example generated from a stream of events), but see below for other options.
*
* Wait for x milliseconds of 'no events' before firing one.
* So an event will either not be handled (busy period),
* or handled after the calm period (so with a delay of x milliseconds)
*
* The second parameter can be used if the timestamps can be calculated from the values on the input iterator
* (by default Date.now() will be used).
* This makes it possible to use this operator on synchronous iterators as well!
*
* @example
* ```typescript
* // imagine a stream of values fired at this pace onto an asyncIterator called itIn:
* // 1, wait 10ms, 2, 3, wait 30ms, 4, wait 10ms, 5, wait 10ms, 6, 7, 8, 9, wait 40ms, 10
* const result = await pipe(itIn, debounce(20), itr8ToArray);
* // => [1, 4, 10]
*
* const valuesWithTimestamps = [
* { value: 1, timestamp: 0 },
* { value: 2, timestamp: 10 },
* { value: 3, timestamp: 10 },
* { value: 4, timestamp: 40 },
* { value: 5, timestamp: 50 },
* { value: 6, timestamp: 60 },
* { value: 7, timestamp: 60 },
* { value: 8, timestamp: 60 },
* { value: 9, timestamp: 60 },
* { value: 10, timestamp: 100 },
* ];
*
* // or get the timestamp from the input values
* const result = await pipe(
* itr8FromIterable(valuesWithTimestamps),
* debounce(20, ([_v, ts]) => ts), // debounce with function that gets timestamp from input
* map(([v, _ts]) => v), // only keep values
* itr8ToArray,
* );
* // => [1, 4, 10]
* ```
*
* @category operators/timeBased
*/
const debounce = <TIn>(cooldownMilliseconds: number) =>
const debounce = <TIn>(
cooldownMilliseconds: number,
getTimestamp = (_value: TIn) => Date.now(),
) =>
powerMap<TIn, TIn, number>(
(nextIn, state) => {
if (nextIn.done) return { done: true };
const newState = Date.now();
const newState = getTimestamp(nextIn.value);
const timePassed = newState - state;
if (timePassed > cooldownMilliseconds) {
return { done: false, value: nextIn.value, state: newState };
Expand Down
33 changes: 31 additions & 2 deletions src/operators/timeBased/throttle.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { assert } from "chai";
import FakeTimers from "@sinonjs/fake-timers";
import { itr8Pushable, itr8ToArray, pipe } from "../../index.js";
import {
itr8FromIterable,
itr8Pushable,
itr8ToArray,
map,
pipe,
} from "../../index.js";
import { sleep } from "../../testUtils/index.js";
import { throttle } from "./throttle.js";

describe("operators/timeBased/throttle.ts", () => {
it("throttle(...) operator works properly", async () => {
it("throttle(...) operator works properly without second argument", async () => {
const clock = FakeTimers.install(); // don't forget to uninstall the clock in a finally block !
try {
const pushIt = itr8Pushable();
Expand Down Expand Up @@ -38,4 +44,27 @@ describe("operators/timeBased/throttle.ts", () => {
clock.uninstall();
}
});

it("throttle(...) operator works properly with second argument", () => {
const valueTimestampTuples = [
[1, 0],
[2, 5],
[3, 5],
[4, 20],
[5, 25],
[6, 30],
[7, 40],
[8, 45],
];

const result = pipe(
itr8FromIterable(valueTimestampTuples),
throttle(15, ([_v, ts]) => ts),
map(([v, _ts]) => v),
itr8ToArray,
);

// and then run the assertions
assert.deepEqual(result, [1, 4, 7]);
});
});
43 changes: 39 additions & 4 deletions src/operators/timeBased/throttle.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,58 @@
import { powerMap } from "../general/powerMap.js";

/**
* Only useful on async iterators.
* Mainly useful on async iterators (for example generated from a stream of events), but see below for other options.
*
* Only throw events at most every x milliseconds.
*
* So when a few events happen quickly, only the first one will be handled,
* So when a few events happen quickly in succession, only the first one will be handled,
* and the next ones will be ignored until enough time (x ms) has passed with
* the previously handled event.
*
* The second parameter can be used if the timestamps can be calculated from the values on the input iterator
* (by default Date.now() will be used).
* This makes it possible to use this operator on synchronous iterators as well!
*
* @example
* ```typescript
* // imagine a stream of values fired at this pace onto an asyncIterator called itIn:
* // 1, wait 5ms, 2, 3, wait 15ms, 4, wait 5ms, 5, wait 5ms, 6wait 10ms, 7wait 5ms, 8
* const result = await pipe(itIn, throttle(15), itr8ToArray);
* // => [1, 4, 7]
*
* const valuesWithTimestamps = [
* { value: 1, timestamp: 0 },
* { value: 2, timestamp: 5 },
* { value: 3, timestamp: 5 },
* { value: 4, timestamp: 20 },
* { value: 5, timestamp: 25 },
* { value: 6, timestamp: 30 },
* { value: 7, timestamp: 40 },
* { value: 8, timestamp: 45 },
* ];
*
* // or get the timestamp from the input values
* const result = await pipe(
* itr8FromIterable(valuesWithTimestamps),
* throttle(15, ([_v, ts]) => ts), // throttle with function that gets timestamp from input
* map(([v, _ts]) => v), // only keep values
* itr8ToArray,
* );
* // => [1, 4, 7]
* ```
* @category operators/timeBased
*/
const throttle = <TIn>(throttleMilliseconds: number) =>
const throttle = <TIn>(
throttleMilliseconds: number,
getTimestamp = (_value: TIn) => Date.now(),
) =>
powerMap<TIn, TIn, number>(
(nextIn, state) => {
if (nextIn.done) {
return { done: true };
}
const now = Date.now();
const now = getTimestamp(nextIn.value);

if (now - state > throttleMilliseconds) {
return { done: false, value: nextIn.value, state: now };
Expand Down
2 changes: 1 addition & 1 deletion src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const thenable = <T>(x: T): TThenable<T> => {
* gain to be made with this.
*
* @example
* ```@typescript
* ```typescript
* // instead of
* for (x of [1, 2, 3]) {
* thenable(x).then((v) => console.log(v));
Expand Down
9 changes: 8 additions & 1 deletion typedoc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ module.exports = {
pluginPages: {
pages: [
{
title: "Roadmap",
name: "Change Log",
source: "./CHANGELOG.md",
// children: [
// { title: 'Configuration', source: 'configuration.md' },
// ],
},
{
name: "Roadmap",
source: "./ROADMAP.md",
// children: [
// { title: 'Configuration', source: 'configuration.md' },
Expand Down

0 comments on commit de2d3f8

Please sign in to comment.