Skip to content

Commit

Permalink
Add timers utils (sergiodxa#259)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiodxa authored Oct 13, 2023
1 parent 968a450 commit 5e3a67f
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 7 deletions.
47 changes: 41 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1304,16 +1304,17 @@ The `eventStream` function is used to create a new event stream response needed
```ts
// app/routes/sse.time.ts
import { eventStream } from "remix-utils/sse/server";
import { interval } from "remix-utils/timers";

export async function loader({ request }: LoaderArgs) {
return eventStream(request.signal, function setup(send) {
let timer = setInterval(() => {
send({ event: "time", data: new Date().toISOString() });
}, 1000);
async function run() {
for await (let _ of interval(1000, { signal: request.signal })) {
send({ event: "time", data: new Date().toISOString() });
}
}

return function clear() {
clearInterval(timer);
};
run();
});
}
```
Expand Down Expand Up @@ -1995,6 +1996,40 @@ export async function loader({ request }: LoaderFunctionArgs) {
}
```

### Timers

The timers utils gives you a way to wait a certain amount of time before doing something or to run some code every certain amount of time.

Using the `wait` combined with an AbortSignal we can cancel a timeout if the user navigates away from the page.

```ts
import { wait } from "remix-utils/timers";

export async function loader({ request }: LoaderFunctionArgs) {
await wait(1000, { signal: request.signal });
// do something after 1 second
}
```

Using the `interval` combined with `eventStream` we could send a value to the client every certain amount of time. And ensure the interval is cancelled if the connection is closed.

```ts
import { eventStream } from "remix-utils/sse/server";
import { interval } from "remix-utils/timers";

export async function loader({ request }: LoaderArgs) {
return eventStream(request.signal, function setup(send) {
async function run() {
for await (let _ of interval(1000, { signal: request.signal })) {
send({ event: "time", data: new Date().toISOString() });
}
}

run();
});
}
```

## Author

- [Sergio Xalambrí](https://sergiodxa.com)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"./honeypot/react": "./build/react/honeypot.js",
"./csrf/server": "./build/server/csrf.js",
"./csrf/react": "./build/react/authenticity-token.js",
"./sec-fetch": "./build/server/sec-fetch.js"
"./sec-fetch": "./build/server/sec-fetch.js",
"./timers": "./build/common/timers.js"
},
"sideEffects": false,
"scripts": {
Expand Down
50 changes: 50 additions & 0 deletions src/common/timers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
interface Options {
signal?: AbortSignal;
}

/**
* Wait for a specified amount of time, accepts a signal to abort the timer.
* @param ms The amount of time to wait in milliseconds
* @param options The options for the timer
* @example
* let controller = new AbortController();
* await wait(1000, { signal: controller.signal });
*/
export function wait(ms: number, options?: Options): Promise<void> {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
if (options?.signal?.aborted) return reject(new TimersError("Aborted"));
return resolve();
}, ms);
if (options?.signal) {
options.signal.addEventListener("abort", () => {
clearTimeout(timeout);
reject(new TimersError("Aborted"));
});
}
});
}

/**
* Get an async iterable that yields on an interval until aborted.
* @param ms The amount of time to wait between intervals, in milliseconds
* @param options The options for the timer
* @returns An async iterable that yields on each intervals
* @example
* let controller = new AbortController();
* for await (let _ of interval(1000, { signal: controller.signal })) {
* // Do something every second until aborted
* }
*/
export async function* interval(ms: number, options?: Options) {
let signal = options?.signal ?? new AbortSignal();
while (!signal.aborted) {
try {
yield await wait(ms, { signal });
} catch {
return;
}
}
}

export class TimersError extends globalThis.Error {}
47 changes: 47 additions & 0 deletions test/common/timers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { wait, interval, TimersError } from "../../src/common/timers";
import { describe, test, expect } from "vitest";

describe("Timers", () => {
describe(wait.name, () => {
test("should resolve after the specified time", async () => {
let start = Date.now();
await wait(100);
let end = Date.now();
expect(end - start).toBeGreaterThanOrEqual(100);
});

test("should reject if aborted", async () => {
let controller = new AbortController();
let start = Date.now();
let promise = wait(100, { signal: controller.signal });
controller.abort();
await expect(promise).rejects.toThrowError(TimersError);
let end = Date.now();
expect(end - start).toBeLessThan(100);
});
});

describe(interval.name, () => {
test("should resolve after the specified time", async () => {
let controller = new AbortController();
let start = Date.now();
let iterator = interval(100, { signal: controller.signal });
let next = await iterator.next();
let end = Date.now();
expect(end - start).toBeGreaterThanOrEqual(100);
expect(next.done).toBe(false);
controller.abort();
});

test("should reject if aborted", async () => {
let controller = new AbortController();
let start = Date.now();
let iterator = interval(100, { signal: controller.signal });
controller.abort();
let next = await iterator.next();
let end = Date.now();
expect(end - start).toBeLessThan(100);
expect(next.done).toBe(true);
});
});
});

0 comments on commit 5e3a67f

Please sign in to comment.