Skip to content

Commit

Permalink
add Decounce and Latest (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pistonight authored Dec 8, 2024
1 parent 9690b84 commit d905675
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 15 deletions.
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pistonite/pure",
"version": "0.0.11",
"version": "0.0.12",
"exports": {
"./fs": "./fs/index.ts",
"./log": "./log/index.ts",
Expand Down
36 changes: 36 additions & 0 deletions sync/Debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* # pure/sync/Debounce
* Debounce executes an action after a certain delay. Any
* call to the action during this delay resets the timer.
*
* ## Warning
* The implementation is naive and does not handle the case
* where the action keeps getting called before the delay,
* in which case the action will never be executed. This
* will be improved in the future...
*/
export class Debounce<TResult> {
private timer: number | undefined;
constructor(
private fn: () => Promise<TResult>,
private delay: number,
) {
this.timer = undefined;
}

public execute(): Promise<TResult> {
if (this.timer !== undefined) {
clearTimeout(this.timer);
}
return new Promise<TResult>((resolve, reject) => {
this.timer = setTimeout(async () => {
this.timer = undefined;
try {
resolve(await this.fn());
} catch (error) {
reject(error);
}
}, this.delay);
});
}
}
77 changes: 77 additions & 0 deletions sync/Latest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Result } from "../result/index.ts";

/**
* # pure/sync/Latest
*
* Latest is a synchronization utility to allow
* only one async operation to be executed at a time,
* and any call will only return the result of the latest
* operation.
*
* ## Example
* In the example below, both call will return the result
* of the second call (2)
* ```typescript
* import { Latest } from "@pistonite/pure/sync";
*
* let counter = 0;
*
* const operation = async () => {
* counter++;
* await new Promise((resolve) => setTimeout(() => {
* resolve(counter);
* }, 1000));
* }
*
* const call = new Latest(operation);
*
* const result1 = call.execute();
* const result2 = call.execute();
* console.log(await result1); // 2
* console.log(await result2); // 2
* ```
*/
export class Latest<TResult> {
private isRunning = false;
private pending: {
resolve: (result: TResult) => void;
reject: (error: unknown) => void;
}[] = [];

constructor(private fn: () => Promise<TResult>) {}

public async execute(): Promise<TResult> {
if (this.isRunning) {
return new Promise<TResult>((resolve, reject) => {
this.pending.push({ resolve, reject });
});
}
this.isRunning = true;
const alreadyPending = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result: Result<TResult, unknown> = { err: "not executed" };
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const fn = this.fn;
result = { val: await fn() };
} catch (error) {
result = { err: error };
}
if (this.pending.length) {
alreadyPending.push(...this.pending);
this.pending = [];
continue;
}
break;
}
this.isRunning = false;
if ("err" in result) {
alreadyPending.forEach(({ reject }) => reject(result.err));
throw result.err;
} else {
alreadyPending.forEach(({ resolve }) => resolve(result.val));
return result.val;
}
}
}
26 changes: 13 additions & 13 deletions sync/SerialEvent.ts → sync/Serial.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { Void, Err, VoidOk } from "../result/index.ts";

/**
* # pure/sync/SerialEvent
* # pure/sync/Serial
*
* An async event that can be cancelled when a new one starts
*
* ## Example
*
* ```typescript
* import { SerialEvent } from "@pistonite/pure/sync";
* import { Serial } from "@pistonite/pure/sync";
*
* // helper function to simulate async work
* const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
*
* // Create the event
* const event = new SerialEvent();
* const event = new Serial();
*
* // The cancellation system uses the Result type
* // and returns an error when it is cancelled
Expand Down Expand Up @@ -53,9 +53,9 @@ import type { Void, Err, VoidOk } from "../result/index.ts";
*
* The callback function receives the current serial number as the second argument, if you need it
* ```typescript
* import { SerialEvent } from "@pistonite/pure/sync";
* import { Serial } from "@pistonite/pure/sync";
*
* const event = new SerialEvent();
* const event = new Serial();
* const promise = event.run(async (shouldCancel, serial) => {
* console.log(serial);
* return {};
Expand All @@ -67,9 +67,9 @@ import type { Void, Err, VoidOk } from "../result/index.ts";
* calling the `shouldCancel` function. This function returns an `Err` if it should be cancelled.
*
* ```typescript
* import { SerialEvent } from "@pistonite/pure/sync";
* import { Serial } from "@pistonite/pure/sync";
*
* const event = new SerialEvent();
* const event = new Serial();
* await event.run(async (shouldCancel, serial) => {
* // do some operations
* ...
Expand All @@ -84,16 +84,16 @@ import type { Void, Err, VoidOk } from "../result/index.ts";
* });
* ```
* It's possible the operation is cheap enough that an outdated event should probably be let finish.
* It's ok in that case to not call `shouldCancel`. The `SerialEvent` class checks it one
* It's ok in that case to not call `shouldCancel`. The `Serial` class checks it one
* last time before returning the result after the callback finishes.
*
* ## Handling cancelled event
* To check if an event is completed or cancelled, simply `await`
* on the promise returned by `event.run` and check the `err`
* ```typescript
* import { SerialEvent } from "@pistonite/pure/sync";
* import { Serial } from "@pistonite/pure/sync";
*
* const event = new SerialEvent();
* const event = new Serial();
* const result = await event.run(async (shouldCancel) => {
* // your code here ...
* );
Expand All @@ -107,16 +107,16 @@ import type { Void, Err, VoidOk } from "../result/index.ts";
* You can also pass in a callback to the constructor, which will be called
* when the event is cancelled. This event is guaranteed to fire at most once per run
* ```typescript
* import { SerialEvent } from "@pistonite/pure/sync";
* import { Serial } from "@pistonite/pure/sync";
*
* const event = new SerialEvent((current, latest) => {
* const event = new Serial((current, latest) => {
* console.log(`Event with serial ${current} is cancelled because the latest serial is ${latest}`);
* });
* ```
*
*
*/
export class SerialEvent {
export class Serial {
private serial: bigint;
private onCancel: SerialEventCancelCallback;

Expand Down
6 changes: 5 additions & 1 deletion sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
* @module
*/
export { RwLock } from "./RwLock.ts";
export { SerialEvent } from "./SerialEvent.ts";
export { Serial } from "./Serial.ts";

// unstable
export { Latest } from "./Latest.ts";
export { Debounce } from "./Debounce.ts";

0 comments on commit d905675

Please sign in to comment.