Skip to content

Commit 9627832

Browse files
authored
Merge pull request #1 from AegisJSProject/patch/initial-release
Initial Release
2 parents 56b6c3b + bf5fdf3 commit 9627832

File tree

14 files changed

+1081
-894
lines changed

14 files changed

+1081
-894
lines changed

.github/workflows/super-linter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
# VALIDATE_JAVASCRIPT_STANDARD: true
9494
# VALIDATE_JSON: true
9595
# VALIDATE_XML: true
96-
VALIDATE_MARKDOWN: true
96+
# VALIDATE_MARKDOWN: true
9797
VALIDATE_YAML: true
9898
# VALIDATE_TYPESCRIPT_ES: true
9999
# VALIDATE_TYPESCRIPT_STANDARD: true

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ importmap.yaml
1717
*.bak
1818
*.env
1919
!*.map
20+
locks.js

CHANGELOG.md

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,76 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### Changed
11-
- Update node version via `.npmrc`
12-
- Update Node CI workflow
13-
- Install & use `@shgysk8zer0/eslint-config`
14-
- Add support for `node --test`, including ignoring tests for publishing
15-
- Update ESLint & super-linter
16-
- Switch to more basic Rollup config
17-
- Update `exports` and `main` accordingly
18-
19-
### Fixed
20-
- Fix missed renaming in README
21-
22-
## Removed
23-
- Remove old ESLint config files
24-
25-
## [v1.1.1] - 2023-09-24
26-
27-
### Added
28-
- Add `unpkg` to `package.json`
29-
- Add badges in README
30-
31-
### Changed
32-
- Update `exports` to `package.json` to handle wider variety
33-
34-
### Fixed
35-
- Fix typo in `fix:js` script
36-
37-
### [v1.1.0] - 2023-07-03
38-
39-
### Changed
40-
- Update to node 20
41-
- Update npm publishing GH Action
42-
43-
## [v1.0.5] - 2023-07-02
44-
45-
### Added
46-
- Add `funding`
47-
48-
### Changed
49-
- Updated GitHub Actions workflows
50-
- Update versioning & lock-file scripts
51-
- Update `.npmignore` & `.gitignore`
52-
53-
## [v1.0.4] - 2023-06-08
54-
55-
### Added
56-
- Install `@shgysk8zer0/npm-utils`
57-
- Add `exports` to package config
58-
59-
### Removed
60-
- Uninstall `rollup`, `eslint`
61-
62-
### Changed
63-
- Use `getConfig()` from `@shgysk8zer0/js-utils/rollup` for rollup config
64-
65-
## [v1.0.3] - 2023-06-01
66-
67-
### Fixed
68-
- Revert to old Release Action, now with permissions & link to changelog
69-
70-
## [v1.0.2] - 2023-06-01
71-
72-
### Fixed
73-
- Fix `changelog-entry` to match `[$version]` instead of `$version`
74-
75-
## [v1.0.1] - 2023-05-31
76-
77-
### Fixed
78-
- Update GitHub Release workflow to use [Auto Release](https://github.com/marketplace/actions/auto-release)
79-
80-
## [v1.0.0] - 2023-05-31
10+
## [v1.0.0] - 2025-03-18
8111

8212
Initial Release

README.md

Lines changed: 150 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
# npm-template
1+
# @aegisjsproject/tempo
22

3-
A template repo for npm packages
3+
Tempo provides advanced debouncing and throttling utilities for fine-grained execution control.
44

5-
[![CodeQL](https://github.com/shgysk8zer0/npm-template/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/shgysk8zer0/npm-template/actions/workflows/codeql-analysis.yml)
6-
![Node CI](https://github.com/shgysk8zer0/npm-template/workflows/Node%20CI/badge.svg)
7-
![Lint Code Base](https://github.com/shgysk8zer0/npm-template/workflows/Lint%20Code%20Base/badge.svg)
5+
[![CodeQL](https://github.com/AegisJSProject/tempo/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/AegisJSProject/tempo/actions/workflows/codeql-analysis.yml)
6+
![Node CI](https://github.com/AegisJSProject/tempo/workflows/Node%20CI/badge.svg)
7+
![Lint Code Base](https://github.com/AegisJSProject/tempo/workflows/Lint%20Code%20Base/badge.svg)
88

9-
[![GitHub license](https://img.shields.io/github/license/shgysk8zer0/npm-template.svg)](https://github.com/shgysk8zer0/npm-template/blob/master/LICENSE)
10-
[![GitHub last commit](https://img.shields.io/github/last-commit/shgysk8zer0/npm-template.svg)](https://github.com/shgysk8zer0/npm-template/commits/master)
11-
[![GitHub release](https://img.shields.io/github/release/shgysk8zer0/npm-template?logo=github)](https://github.com/shgysk8zer0/npm-template/releases)
9+
[![GitHub license](https://img.shields.io/github/license/AegisJSProject/tempo.svg)](https://github.com/AegisJSProject/tempo/blob/master/LICENSE)
10+
[![GitHub last commit](https://img.shields.io/github/last-commit/AegisJSProject/tempo.svg)](https://github.com/AegisJSProject/tempo/commits/master)
11+
[![GitHub release](https://img.shields.io/github/release/AegisJSProject/tempo?logo=github)](https://github.com/AegisJSProject/tempo/releases)
1212
[![GitHub Sponsors](https://img.shields.io/github/sponsors/shgysk8zer0?logo=github)](https://github.com/sponsors/shgysk8zer0)
1313

14-
[![npm](https://img.shields.io/npm/v/@shgysk8zer0/npm-template)](https://www.npmjs.com/package/@shgysk8zer0/npm-template)
15-
![node-current](https://img.shields.io/node/v/@shgysk8zer0/npm-template)
16-
![npm bundle size gzipped](https://img.shields.io/bundlephobia/minzip/@shgysk8zer0/npm-template)
17-
[![npm](https://img.shields.io/npm/dw/@shgysk8zer0/npm-template?logo=npm)](https://www.npmjs.com/package/@shgysk8zer0/npm-template)
14+
[![npm](https://img.shields.io/npm/v/@aegisjsproject/tempo)](https://www.npmjs.com/package/@aegisjsproject/tempo)
15+
![node-current](https://img.shields.io/node/v/@aegisjsproject/tempo)
16+
![npm bundle size gzipped](https://img.shields.io/bundlephobia/minzip/@aegisjsproject/tempo)
17+
[![npm](https://img.shields.io/npm/dw/@aegisjsproject/tempo?logo=npm)](https://www.npmjs.com/package/@aegisjsproject/tempo)
1818

19-
[![GitHub followers](https://img.shields.io/github/followers/shgysk8zer0.svg?style=social)](https://github.com/shgysk8zer0)
20-
![GitHub forks](https://img.shields.io/github/forks/shgysk8zer0/npm-template.svg?style=social)
21-
![GitHub stars](https://img.shields.io/github/stars/shgysk8zer0/npm-template.svg?style=social)
19+
[![GitHub followers](https://img.shields.io/github/followers/AegisJSProject.svg?style=social)](https://github.com/AegisJSProject)
20+
![GitHub forks](https://img.shields.io/github/forks/AegisJSProject/tempo.svg?style=social)
21+
![GitHub stars](https://img.shields.io/github/stars/AegisJSProject/tempo.svg?style=social)
2222
[![Twitter Follow](https://img.shields.io/twitter/follow/shgysk8zer0.svg?style=social)](https://twitter.com/shgysk8zer0)
2323

2424
[![Donate using Liberapay](https://img.shields.io/liberapay/receives/shgysk8zer0.svg?logo=liberapay)](https://liberapay.com/shgysk8zer0/donate "Donate using Liberapay")
@@ -27,3 +27,138 @@ A template repo for npm packages
2727
- [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
2828
- [Contributing](./.github/CONTRIBUTING.md)
2929
<!-- - [Security Policy](./.github/SECURITY.md) -->
30+
31+
# @aegisjsproject/tempo
32+
33+
A lightweight JavaScript library for debouncing and throttling functions using modern, native web APIs.
34+
35+
---
36+
37+
## Overview
38+
39+
`@aegisjsproject/tempo` provides robust debouncing and throttling utilities by leveraging the browser's latest scheduling and locking APIs. Using methods like `scheduler.postTask` and `navigator.locks.request`, the library offers precise task scheduling with native priority support, efficient cancellation via AbortSignals, and streamlined resource management.
40+
41+
---
42+
43+
## Features
44+
45+
- **Debounce Function**
46+
Debounces a callback function by scheduling it with `scheduler.postTask`. It supports custom delays, task priorities (`user-blocking`, `user-visible`, `background`), and cancellation through AbortSignals.
47+
48+
- **Throttle Function**
49+
Throttles a callback by combining `scheduler.postTask` with `navigator.locks.request` for controlled, queued execution. It provides options for lock naming, immediate failure or lock stealing, and built-in delay management.
50+
51+
---
52+
53+
## Advantages of Modern Methods
54+
55+
- **Native Integration:**
56+
Leverages browser-native APIs for task scheduling and lock management, reducing dependency overhead and resulting in smaller bundle sizes.
57+
58+
- **Improved Performance:**
59+
Native scheduling with `scheduler.postTask` allows tasks to be queued based on system and user priorities, leading to smoother and more responsive applications.
60+
61+
- **Enhanced Control:**
62+
With built-in support for AbortSignals and fine-grained control over task priorities, developers can efficiently manage resource-intensive operations.
63+
64+
- **Optimized Concurrency:**
65+
The use of `navigator.locks.request` ensures that throttled tasks handle concurrent execution gracefully without resorting to heavy third-party libraries.
66+
67+
---
68+
69+
## Installation
70+
71+
Install the package via npm:
72+
73+
```bash
74+
npm install @aegisjsproject/tempo
75+
```
76+
77+
---
78+
79+
## Usage
80+
81+
Import the desired functions into your project:
82+
83+
```js
84+
import { debounce, throttle } from '@aegisjsproject/tempo';
85+
```
86+
87+
### Usage with a CDN and `<script type="importmap">`
88+
89+
```html
90+
<script type="importmap">
91+
{
92+
"imports": {
93+
"@aegisjsproject/tempo": "https://unpkg.com/@aegisjsproject[:version]/tempo.js"
94+
}
95+
}
96+
</script>
97+
```
98+
### Debounce Example
99+
100+
```js
101+
const debouncedAction = debounce(() => {
102+
// Your callback code here.
103+
}, {
104+
delay: 300,
105+
priority: 'user-visible',
106+
signal: myAbortSignal, // Optional: provide an AbortSignal for cancellation.
107+
});
108+
```
109+
110+
### Throttle Example
111+
112+
```js
113+
const throttledAction = throttle(() => {
114+
// Your callback code here.
115+
}, {
116+
lockName: 'uniqueLockName', // Optional: defaults to a random UUID.
117+
delay: 200,
118+
priority: 'background',
119+
ifAvailable: true, // Immediately fail if the lock is unavailable.
120+
signal: myAbortSignal, // Optional: provide an AbortSignal for cancellation.
121+
});
122+
```
123+
124+
---
125+
126+
## API Documentation
127+
128+
### `debounce(callback, options)`
129+
130+
- **Parameters:**
131+
- `callback` *(Function)*: The function to debounce.
132+
- `options` *(Object, optional)*:
133+
- `delay` *(number)*: The debounce delay in milliseconds.
134+
- `priority` *("user-blocking" | "user-visible" | "background")*: The task priority.
135+
- `signal` *(AbortSignal)*: Signal to cancel the debounced call.
136+
- `thisArg` *(any, default: null)*: The `this` context for the callback.
137+
138+
- **Returns:**
139+
A debounced version of the input function.
140+
141+
- **Throws:**
142+
- `TypeError` if the callback is not a function.
143+
- `Error` if the provided AbortSignal is already aborted.
144+
145+
### `throttle(callback, options)`
146+
147+
- **Parameters:**
148+
- `callback` *(Function)*: The function to throttle.
149+
- `options` *(Object, optional)*:
150+
- `lockName` *(string, default: crypto.randomUUID())*: Name for the lock.
151+
- `delay` *(number)*: Delay in milliseconds after task execution.
152+
- `priority` *("user-blocking" | "user-visible" | "background")*: The task priority.
153+
- `ifAvailable` *(boolean, default: true)*: Fail immediately if the lock is not available.
154+
- `steal` *(boolean, default: false)*: Allow stealing of the lock.
155+
- `mode` *("shared" | "exclusive", default: "exclusive")*: The lock mode (only 'exclusive' is supported).
156+
- `thisArg` *(any, default: null)*: The `this` context for the callback.
157+
- `signal` *(AbortSignal)*: Signal to cancel the throttled call.
158+
159+
- **Returns:**
160+
A throttled version of the input function.
161+
162+
- **Throws:**
163+
- `TypeError` if the callback is not a function or if both `steal` and `ifAvailable` are set to true.
164+
- `Error` if the provided AbortSignal is already aborted.

consts.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

eslint.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { node } from '@shgysk8zer0/eslint-config';
1+
import { browser } from '@shgysk8zer0/eslint-config';
22

3-
export default node({ files: ['**/*.js'], ignores: ['**/*.min.js', '**/*.cjs', '**/*.mjs'] });
3+
export default browser({ files: ['**/*.js'], ignores: ['**/*.min.js', '**/*.cjs', '**/*.mjs'] });

index.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

index.test.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

locks.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import '@shgysk8zer0/polyfills';
2+
3+
const locks = Object.freeze({
4+
shared: new Map(),
5+
exclusive: new Map(),
6+
});
7+
8+
globalThis.Lock = class Lock {
9+
#mode;
10+
#name;
11+
12+
constructor(name, mode) {
13+
this.#name = name;
14+
this.#mode = mode;
15+
}
16+
17+
get name() {
18+
return this.#name;
19+
}
20+
21+
get mode() {
22+
return this.#mode;
23+
}
24+
};
25+
26+
globalThis.navigator = {
27+
locks: {
28+
query() {
29+
return Object.freeze({
30+
shared: Array.from(locks.shared.values()),
31+
exclusive: Array.from(locks.exclusive.values()),
32+
});
33+
},
34+
async request(name, {
35+
delay = 0,
36+
mode = 'exclusive',
37+
signal,
38+
}, callback) {
39+
if (! (mode in locks)) {
40+
throw new TypeError(`Invalid lock mode "${mode}".`);
41+
} else if (signal instanceof AbortSignal && signal.aborted) {
42+
throw signal.reason;
43+
} else if (! locks[mode].has(name)) {
44+
const lock = new Lock(name, mode);
45+
const controller = new AbortController();
46+
const { resolve, reject, promise } = Promise.withResolvers();
47+
controller.signal.addEventListener('abort', ({ target }) => reject(target.reason), { once: true });
48+
locks[mode].set(name, lock);
49+
50+
try {
51+
let timeout = NaN;
52+
53+
const wait = typeof delay === 'number' ? new Promise(resolve => {
54+
timeout = setTimeout(resolve, delay);
55+
}) : Promise.resolve();
56+
57+
if (signal instanceof AbortSignal) {
58+
signal.addEventListener('abort', ({ target }) => {
59+
controller.abort(target.reason);
60+
61+
if (! Number.isNaN(timeout)) {
62+
clearTimeout(timeout);
63+
}
64+
});
65+
}
66+
67+
await Promise.race([wait, promise]).then(() => {
68+
locks[mode].get(name) === lock
69+
? Promise.try(() => callback(lock)).then(resolve, reject)
70+
: Promise.try(callback).then(resolve, reject);
71+
}).catch(err => reject(err));
72+
73+
} catch(err) {
74+
reject(err);
75+
} finally {
76+
/* eslint no-unsafe-finally: off */
77+
const [result, err] = await promise.then(result => [result, null]).catch(err => [null, err]);
78+
79+
if (locks[mode].get(name) === lock) {
80+
locks[mode].delete(name);
81+
}
82+
83+
if (! controller.signal.aborted) {
84+
controller.abort(err);
85+
}
86+
87+
if (err instanceof Error) {
88+
throw err;
89+
} else {
90+
return result;
91+
}
92+
}
93+
}
94+
}
95+
}
96+
};

0 commit comments

Comments
 (0)