diff --git a/package.json b/package.json index a3b8e99..3bbda5c 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "url": "https://github.com/webdeveric/utils/issues" }, "homepage": "https://github.com/webdeveric/utils/#readme", - "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620", + "packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26", "scripts": { "clean": "rimraf ./dist/ ./cache/ ./coverage/", "prebuild": "pnpm clean", @@ -88,7 +88,7 @@ "@commitlint/config-conventional": "^21.0.2", "@commitlint/types": "^21.0.1", "@types/node": "^24.13.2", - "@typescript/native-preview": "7.0.0-dev.20260615.1", + "@typescript/native-preview": "7.0.0-dev.20260617.2", "@vitest/coverage-v8": "^4.1.9", "@webdeveric/eslint-config-ts": "^0.12.0", "@webdeveric/prettier-config": "^0.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 890a4a1..138e78d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^24.13.2 version: 24.13.2 '@typescript/native-preview': - specifier: 7.0.0-dev.20260615.1 - version: 7.0.0-dev.20260615.1 + specifier: 7.0.0-dev.20260617.2 + version: 7.0.0-dev.20260617.2 '@vitest/coverage-v8': specifier: ^4.1.9 version: 4.1.9(vitest@4.1.9) @@ -963,50 +963,50 @@ packages: resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-EHrtoVGEEhIhsnGe+b8w0FoM9JfIw5SkoPwO8ifaU0PrYm2UbyPbj2I2hOTxtk458t0irvGz2+8cshylBRbKng==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-fr6hLBO+XOUJQ+WDRIbGLDhVOL1vDlrnn086U6QKVu2aT5XtGOcas1KIl2NJOihSgEI5ZRaUGrQsGpeXu0LNwQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-BcDA56hkk6mrUpysaOVvrdACoX5d2SF1JTwHMoNjT1KysBicExS2wlH0eN0L01iDeqtB73XHl7A4zrKFmKzrBg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-tJy6NnyvCqD2NgW+izXJ4D8d/xve5W+lkbDMPsX60aqqnS3f9yMuhHLIbj9vnIfB7NMv+xeV5uC2DK3g7FRDZw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-TDAlBpyYCF7Z+ELTH+1tabDE6W3shl+H+Z+nmzaQio1I8pFvbwt2iLlE0Rc9CpRdIeaqr0ppMEgXHoeV3fZFWA==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-H78Hs/Ycr+i+b3+UToAs16SX+2s/ZZ2jDVXx+EIQ2qxVkQWFkiAg99uFDC6nthLrjw9dyIICwby6ZnwbFW47iQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-YRXRS/7ZqrDKXBJhFQcZiOdnHRuRbWoL/QkggpoGfhIuSAh4HvTU0WEUnPM+jRnH2kUMfsSgd8EAnPvmuP7/VA==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-uVBcvZ7Z9H3BkIOyC1wpsGb+Mfvnl+Ss6V+4yLJQA9Rh3ghpBIZj0ZAWH7XwdjoazU2zyj8IiJJB5CgeIp3d9w==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-/QDmRWt6abB7fw3yMchjlyDXej/7Dr8mYG4wvGGf6c1hc5Il5UpDQWNHejatdeKvAu+sszz8JhTuL8et47fTTg==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-KKW/eqJRe1xm2omYPZJvOu7xDspJPVKb8Z6v0MwgzOnRtRTcGRj+nRJX9h9LKTWxyv5D7TU6zNzLlJvNfP8kAQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-WTJOLoe2rxT0W1i8ndWk2MKrakxRFNki537JZxvKAmSTbyOZznHlW3O3dbryUtTBYA716DDqS3ci24kuIfvBdg==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-XsLqZ6nz+fwQqR0lAHYsCOI2BwTsp4iBgdHFIwfjznBnyctPX5WxKZSqmnRxrhQxF+6qKwXplwcfP+2bOCWf/A==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-4cSCpXG7um18nwmLdU/SjoTv3OcO38/ufTiy1oWVccgGHLJqppiOP9/o+ElKIWhvrp78IaGy8+h3YqEjQ4/pcQ==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-+nSSVVhp17R5KHSQw2uO0CGtTGCb9bUeQ/INcUCycVfO0tguwdyoYiuL8enlVkh059MgZW6LjqIhKXQHkGW+Lw==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260615.1': - resolution: {integrity: sha512-JJ8X1l7H1GrnseK1k30qfQqB8Pz6jw3IALZVIj5oXQeRbUCe0Wx3ljkJEmOpunogdhEfA8IlOggskbUjVsXKBQ==} + '@typescript/native-preview@7.0.0-dev.20260617.2': + resolution: {integrity: sha512-PvEU1RcMgON18fb65PW8xkAD1X270kjvC6BjE3c6YMe6T4cmXxOPYuNQWGIBTpeNSxN2yW1qUlHdMxKgUYuKxQ==} engines: {node: '>=16.20.0'} hasBin: true @@ -1678,6 +1678,10 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -1705,8 +1709,8 @@ packages: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + es-to-primitive@1.3.1: + resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} engines: {node: '>= 0.4'} es-toolkit@1.47.1: @@ -4904,36 +4908,36 @@ snapshots: '@typescript-eslint/types': 8.61.1 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260615.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260615.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260615.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260615.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260615.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260615.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260615.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260617.2': optional: true - '@typescript/native-preview@7.0.0-dev.20260615.1': + '@typescript/native-preview@7.0.0-dev.20260617.2': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260615.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260615.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260615.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260615.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260615.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260615.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260615.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260617.2 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260617.2 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260617.2 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260617.2 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260617.2 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260617.2 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260617.2 '@ungap/structured-clone@1.3.1': {} @@ -5642,6 +5646,13 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -5656,7 +5667,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 + es-to-primitive: 1.3.1 function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 @@ -5720,8 +5731,10 @@ snapshots: dependencies: hasown: 2.0.4 - es-to-primitive@1.3.0: + es-to-primitive@1.3.1: dependencies: + es-abstract-get: 1.0.0 + es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 diff --git a/release.config.mjs b/release.config.mjs index 1ab58c7..c605ff8 100644 --- a/release.config.mjs +++ b/release.config.mjs @@ -9,6 +9,22 @@ export default { '@semantic-release/commit-analyzer', { releaseRules: [ + { + type: 'chore', + scope: 'deps', + release: 'patch', + }, + // Use this one when we want to release a minor version for dependency updates. + { + type: 'chore', + scope: 'deps-minor', + release: 'minor', + }, + { + type: 'chore', + scope: 'deps-dev', + release: false, + }, { type: 'docs', release: 'patch', @@ -25,8 +41,29 @@ export default { ], }, ], - '@semantic-release/release-notes-generator', - '@semantic-release/npm', + [ + '@semantic-release/release-notes-generator', + { + preset: 'conventionalcommits', + presetConfig: { + types: [ + { type: 'feat', section: 'Features' }, + { type: 'fix', section: 'Bug Fixes' }, + { type: 'chore', scope: 'deps', section: 'Dependencies' }, + { type: 'chore', scope: 'deps-minor', section: 'Dependencies' }, + { type: 'docs', section: 'Documentation' }, + { type: 'refactor', section: 'Refactoring' }, + { type: 'chore', scope: 'spelling', section: 'Other' }, + ], + }, + }, + ], + [ + '@semantic-release/npm', + { + provenance: true, + }, + ], [ '@semantic-release/github', { diff --git a/src/unique.ts b/src/unique.ts index 3f81c7d..a25bf3f 100644 --- a/src/unique.ts +++ b/src/unique.ts @@ -1,20 +1,67 @@ -export function* unique( - items: Iterable, - getIdentity: (item: T) => unknown = (item) => item, -): Generator { - if (items instanceof Set) { - yield* items; - } else { - const ids = new Set(); - - for (const item of items) { - const id = getIdentity(item); - - if (!ids.has(id)) { - ids.add(id); - - yield item; - } - } +import { isAsyncIterable } from './predicate/isAsyncIterable.js'; +import { isIterable } from './predicate/isIterable.js'; + +export type UniqueOptions = { + /** + * Return a custom ID to uniquely identify an item. + */ + identity?: (item: Type) => unknown; + /** + * Return `true` to yield the item. + * + * Use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/filter when available. + */ + filter?: (item: Type) => boolean; +}; + +export function unique(items: AsyncIterable, options?: UniqueOptions): AsyncIterable; + +export function unique(items: Iterable, options?: UniqueOptions): Iterable; + +export function unique(items: string, options?: UniqueOptions): Iterable; + +export function unique( + items: AsyncIterable | Iterable, + options: UniqueOptions = {}, +): AsyncIterable | Iterable { + const ids = new Set(); + const { filter, identity } = options; + + if (typeof items === 'string' || isIterable(items)) { + return { + *[Symbol.iterator]() { + for (const item of items) { + if (!filter || filter(item) === true) { + const id = identity?.(item) ?? item; + + if (!ids.has(id)) { + ids.add(id); + + yield item; + } + } + } + }, + }; + } + + if (isAsyncIterable(items)) { + return { + async *[Symbol.asyncIterator]() { + for await (const item of items) { + if (!filter || filter(item) === true) { + const id = identity?.(item) ?? item; + + if (!ids.has(id)) { + ids.add(id); + + yield item; + } + } + } + }, + }; } + + throw new TypeError('items must be an Iterable or AsyncIterable'); } diff --git a/test/unique.test.ts b/test/unique.test.ts index e8c0117..ea37420 100644 --- a/test/unique.test.ts +++ b/test/unique.test.ts @@ -1,19 +1,9 @@ -import { isGeneratorFunction } from 'node:util/types'; - import { describe, expect, it } from 'vitest'; import { unique } from '../src/unique.js'; describe('unique()', () => { - it('Is a generator', () => { - expect(isGeneratorFunction(unique)).toBeTruthy(); - }); - - it('Delegate iteration to Set', () => { - expect([...unique(new Set([1, 2, 3]))]).toEqual([1, 2, 3]); - }); - - it('Yields unique items', () => { + it('Yields unique items from an Iterable', () => { // cSpell:ignore abbccc expect([...unique('abbccc')]).toEqual(['a', 'b', 'c']); @@ -25,12 +15,59 @@ describe('unique()', () => { ['one', 'test'], ['two', 'test'], ]), - (item) => item[1], + { + identity: (item) => item[1], + }, ), ]).toEqual([['one', 'test']]); }); + it('Yields unique items from an AsyncIterable', async () => { + const demo = async function* (): AsyncGenerator { + yield 1; + yield 2; + yield 2; + yield 3; + yield 3; + yield 3; + }; + + await expect(Array.fromAsync(unique(demo()))).resolves.toEqual([1, 2, 3]); + + await expect( + Array.fromAsync( + unique(demo(), { + filter(item) { + return item > 1; + }, + }), + ), + ).resolves.toEqual([2, 3]); + }); + it('Can use a function to identify uniqueness', () => { - expect([...unique('AaBbCc', (item) => item.toLowerCase())]).toEqual(['A', 'B', 'C']); + expect([ + ...unique('AaBbCc', { + identity(item) { + return item.toLowerCase(); + }, + }), + ]).toEqual(['A', 'B', 'C']); + }); + + it('Can filter items', () => { + expect([ + ...unique('ABC', { + filter(item) { + return item !== 'C'; + }, + }), + ]).toEqual(['A', 'B']); + }); + + it('Throws when not given an Iterable or AsyncIterable', () => { + expect(() => { + unique(false as unknown as Iterable); + }).toThrow(); }); }); diff --git a/tsconfig.test.json b/tsconfig.test.json index 8a5e557..7825099 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "noEmit": true, + "lib": ["ESNext", "DOM"], "verbatimModuleSyntax": true, "tsBuildInfoFile": "./cache/test.tsbuildinfo", "types": ["node"]