Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 31 additions & 26 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

permissions:
contents: read

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
strategy:
matrix:
node: [22, 24]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '18.x'

- name: install deps
run: yarn

- name: test
run: yarn test

- name: build lib
run: yarn build
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install Yarn
run: npm install --global yarn@1.22.22
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Typecheck
run: yarn typecheck
- name: Test including performance regression budgets
run: yarn test
- name: Benchmark against the pre-refactor implementation
run: yarn benchmark
- name: Check packed imports and legacy TypeScript consumers
run: node scripts/check-package.cjs
- name: Upload performance measurements
uses: actions/upload-artifact@v4
with:
name: perf-node-${{ matrix.node }}
path: perf-results.json
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,6 @@ dist
# SvelteKit build / generate output
.svelte-kit

# End of https://www.toptal.com/developers/gitignore/api/node
# End of https://www.toptal.com/developers/gitignore/api/node
# Generated performance measurements
perf-results.json
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

## Unreleased

- Add optional `getTaskKey` for indexed task deduplication and cache lookup.
- Share one task record and result Promise per unique task, and consume serial
queues with a cursor to reduce scheduling work.
- Fix cache expiration and failed-task retries while unrelated tasks are running.
- Settle task failures reliably, including executor and cache-policy exceptions.
- Handle empty dispatches and missing batch results; preserve result order and
duplicate task positions.
- Preserve the configured batch size in serial mode instead of forcing one item.
- Compare cyclic plain objects and arrays safely; use reference identity for
opaque objects such as Map, Set and class instances.
- Correct the built package entry points and include generated TypeScript declarations.
- Retain the published `dist/index.js` and `dist/types.d.ts` paths, CommonJS
`import = require` syntax, and the structural public instance type. Check real
packed consumers in CI.
- Expand behavior and type coverage, add deterministic performance regression
tests, and benchmark against the pre-refactor implementation in CI.
- Rewrite the README around request deduplication, batching and TTL caching,
with runnable examples and clearer cache / execution semantics.
- Update package discovery metadata and include the MIT license text.

This section describes changes awaiting release. The latest published npm version
is 1.0.1 (2024-03-21).

The public signatures are retained, but some bug fixes change observable behavior.
See [upgrade notes](docs/upgrading.md) before releasing or upgrading.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Saiya

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
67 changes: 67 additions & 0 deletions docs/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Performance and testing

[Back to the README](../readme.md)

This document describes the upcoming release. `getTaskKey` is not available in
npm 1.0.1; see the [changelog](../CHANGELOG.md).
For many tasks or large object inputs, supply a stable, inexpensive `getTaskKey`:

```ts
import TaskSchedule from 'async-task-schedule'

const users = new TaskSchedule({
doTask: async (task: { tenantId: string; userId: string }) => {
const response = await fetch(`/tenants/${encodeURIComponent(task.tenantId)}/users/${encodeURIComponent(task.userId)}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
},
getTaskKey: task => JSON.stringify([task.tenantId, task.userId]),
})
```

Keys take precedence over `isSameTask`. Equal keys share execution and cached results;
the first task's parameters are used. Include every input that affects the result
(e.g. tenant, user, permissions, or query options). Keys must be pure and stable
throughout execution and caching; do not mutate task identity after dispatch.
A symbol must be reused, rather than created anew on each call. Keys use `Map` equality:
`1` differs from `'1'`, `NaN` equals `NaN`, and `0` equals `-0`.
Without `getTaskKey`, deep comparison or your custom `isSameTask` continues to apply.

Each unique task has one record and a shared result Promise. Duplicate requests
subscribe to that result; completing a task no longer scans every waiting request.
The indexed path avoids linear searches for pending, running, and cached tasks.
Numeric TTLs skip cache scans until the next expiration; function TTL policies are
evaluated on completion and on subsequent cache access. Cache expiration and
failed-task retry eligibility apply even while unrelated work is active. Existing
waiters keep their original result Promise after a cache entry expires.
`cleanCache()` still waits for pending/running work before clearing entries.

Default deep comparison supports plain objects, arrays (including cyclic values),
Dates, and RegExps. Other objects such as Map, Set, typed arrays, and class instances
use reference identity; supply a task key or custom comparator when those inputs
need semantic equality. An array passed to `dispatch` always means multiple tasks;
wrap an array-valued single task in an object.

If a TTL policy throws during completion, that task settles with an Error (a single
request rejects, and a multi-task request receives an Error entry). If it throws
on later cache access, the dispatch rejects and that cache entry is removed.
Neither path leaves background rejections or unresolved task records.

Run `yarn test` for behavior tests, deterministic performance regression budgets,
and coverage; `yarn test:perf` runs just the performance regression suite.
`yarn typecheck` checks source and test/type assertions. `yarn benchmark` builds
the library and compares it with the indexed pre-refactor commit
`f2e07d2c2e358c634a5fb075163fdbc2ef353ccd` in this repository (Git history is required).
It covers batch, individual, overlapping, and serial requests at 100/500/1000
unique tasks, verifies outputs and execution counts, and records cold/hot-cache
five-run median timings and key-call counts in `perf-results.json`.

CI on Node 22/24 gates deterministic work budgets: one key extraction per submitted
item, no completion-time identity lookups, and one execution per distinct task.
It also runs the elapsed-time benchmark and uploads its JSON report for comparison.
Elapsed-time measurements include timer and Promise overhead; they are diagnostic,
not machine-independent pass/fail thresholds. No runtime dependencies are added.

`maxBatchCount` remains the batch size, not a global concurrency limit in parallel
mode. Unlimited caching can retain results indefinitely; use a TTL or `cleanCache()`
when appropriate. The existing batching strategies are preserved.
50 changes: 50 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Upgrading from 1.0.1

These notes describe the proposed next release. It has not been published yet.

## Import and API compatibility

The constructor options, dispatch overloads, cache-clearing method, running
indicator and static utilities remain available. `getTaskKey` is optional;
existing custom comparators keep working when it is omitted.

The package retains `dist/index.js`, `dist/index.es.js` and `dist/types.d.ts`.
CommonJS `require('async-task-schedule')`, TypeScript `import Schedule =
require('async-task-schedule')`, and default imports with `esModuleInterop` are
checked against the actual packed package. Exported strategy types and the
structural public instance type remain usable.

Private runtime fields and the undocumented `defaultOptions` static field are
implementation details. The refactor changes these internals.

## Observable corrections

The next release follows the documented behavior more closely. It is not a
guarantee of identical behavior for applications relying on bugs in 1.0.1.

| Area | 1.0.1 behavior | Next release / upgrade action |
| --- | --- | --- |
| Single-task failure | Could fulfill with an `Error` value. | Rejects. Use `try/catch` or `.catch()`; array dispatch still returns per-item errors. |
| Parallel batch execution | Batches could execute serially even when configured as parallel. | Parallel batches start together. Set `taskExecStrategy: 'serial'` if the backend requires sequential requests. |
| Serial batch size | Serial mode forced a batch size of one. | Honors `maxBatchCount`. Set it to `1` explicitly to preserve one-item batches. |
| Default task comparison | Could treat different Map/Set values as equal; class instances and typed arrays were compared through enumerable fields. | Opaque objects use reference identity. Supply a domain-specific `isSameTask` or stable `getTaskKey` if separate instances should identify the same task. |
| Other equality boundaries | Ignored enumerable symbols, sparse-array lengths and prototype differences; cycles could overflow. | Considers those boundaries and supports cyclic plain objects / arrays. Review custom object inputs or specify identity explicitly. |
| Cache expiration and retries | Could reuse expired or failed entries while unrelated work was active. | Expiration and retry eligibility apply on dispatch even while other work runs. This may cause additional executions that the old bug suppressed. |
| Invalid batch size | Invalid values were not validated reliably. | Nonzero batch sizes must be positive integers; invalid values throw during construction. |
| TTL callback failures | Could escape background work and leave callers unresolved. | Completion failures settle the task as an error; cache-access failures reject dispatch and evict the entry. |

Cache-policy callbacks should be pure: they can now run at completion as well as
on later cache access. Comparator callbacks should be pure and symmetric; the
indexed implementation does not preserve the old number or order of callback calls.

The batch-size option does not impose a global concurrency limit in parallel
mode. Keep existing backpressure requirements explicit when upgrading.

## Validation scope

Behavior tests cover errors, batching / waiting combinations, deduplication, TTL,
retry and cache clearing. Deterministic performance tests check identity work and
execution counts. Packed-consumer tests check imports, declarations, old file
paths and execution. This verifies supported interfaces and covered behaviors;
it cannot establish compatibility with every application's use of old internals
or previously incorrect behavior.
32 changes: 24 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
{
"name": "async-task-schedule",
"version": "1.0.1",
"description": "schedule async tasks",
"main": "dist/index.umd.js",
"description": "Batch and deduplicate async requests with TTL caching.",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/types.d.ts",
"scripts": {
"build": "vite build",
"build": "vite build && node scripts/compat-package.cjs",
"benchmark": "npm run build && node scripts/benchmark.cjs",
"typecheck": "tsc --noEmit && tsc --project tsconfig.test.json",
"test": "vitest run --coverage",
"release": "yarn test && yarn build && yarn publish"
"test:perf": "vitest run test/performance.spec.ts",
"release": "yarn test && yarn build && yarn publish",
"test:package": "npm run build && node scripts/check-package.cjs"
},
"keywords": [
"async task",
"schedule",
"task",
"batch",
"batching",
"deduplication",
"request-deduplication",
"coalescing",
"promise",
"async",
"dispatch"
"cache",
"ttl",
"typescript"
],
"repository": "https://github.com/oe/async-task-schedule.git",
"author": "Saiya <oklark@outlook.com>",
"license": "MIT",
"private": false,
"source": "src/index.ts",
"files": [
"dist"
],
"devDependencies": {
"@types/node": "^20.0.0",
"@vitest/coverage-v8": "2.1.5",
"typescript": ">=3.0.0",
"vite": "^5.4.11",
"vitest": "^2.1.5",
"vite-plugin-dts": "^4.3.0"
},
"homepage": "https://github.com/oe/async-task-schedule#readme",
"bugs": {
"url": "https://github.com/oe/async-task-schedule/issues"
}
}
Loading
Loading