From 6a56de943ce5d968bb627a129a2b7e530bb40e53 Mon Sep 17 00:00:00 2001 From: Dirk Holtwick Date: Fri, 21 Jun 2024 21:08:09 +0200 Subject: [PATCH 1/2] feat: typescript using for useDispose --- .gitignore | 1 + package.json | 18 +++++------ src/common/data/object.spec.ts | 24 ++++++++++++-- src/common/dispose-defer.spec.ts | 54 +++++++++++++++++++------------- src/common/dispose-defer.ts | 19 +++++++++++ src/common/dispose-types.ts | 6 ++++ src/common/dispose-utils.spec.ts | 20 ++++++++++++ src/common/dispose-utils.ts | 30 +++++++++++++----- src/node/files-async.ts | 31 ++++++++++++++++-- src/node/fs.ts | 1 - tsconfig.json | 3 +- vitest.config.ts | 3 ++ 12 files changed, 167 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index aa7d9242..e833970f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ _archive .npmrc* lib docs +_sandbox diff --git a/package.json b/package.json index f002801d..57bfa8a4 100644 --- a/package.json +++ b/package.json @@ -65,16 +65,16 @@ "watch": "nr build -- --watch src" }, "devDependencies": { - "@antfu/eslint-config": "^2.16.1", + "@antfu/eslint-config": "^2.21.1", "@antfu/ni": "^0.21.12", - "@types/node": "^20.12.8", - "@vitest/coverage-v8": "^1.5.3", - "esbuild": "^0.20.2", - "eslint": "^9.1.1", - "tsup": "^8.0.2", + "@types/node": "^20.14.7", + "@vitest/coverage-v8": "^1.6.0", + "esbuild": "^0.21.5", + "eslint": "<9", + "tsup": "^8.1.0", "typedoc": "^0.25.13", - "typescript": "^5.4.5", - "vite": "^5.2.10", - "vitest": "^1.5.3" + "typescript": "^5.5.2", + "vite": "^5.3.1", + "vitest": "^1.6.0" } } diff --git a/src/common/data/object.spec.ts b/src/common/data/object.spec.ts index 1c6a88c3..1ab1c228 100644 --- a/src/common/data/object.spec.ts +++ b/src/common/data/object.spec.ts @@ -1,7 +1,6 @@ -import exp from 'node:constants' import { useDispose } from '../dispose-defer' import { Emitter } from '../msg/emitter' -import { isNotNull } from './is' +import { isBoolean, isNotNull } from './is' import { objectInclusivePick, objectMap, objectMergeDisposable, objectOmit, objectPick, objectPlain } from './object' describe('object.spec', () => { @@ -100,6 +99,23 @@ describe('objectPlain', () => { expect(result).toEqual(obj) }) + it('should handle bool', async () => { + const x = objectPlain({ test: true, fdsf: { fsdafs: false } }, { + transformer: (obj) => { + if (isBoolean(obj)) + return +obj + }, + }) + expect(x).toMatchInlineSnapshot(` + Object { + "fdsf": Object { + "fsdafs": 0, + }, + "test": 1, + } + `) + }) + it('should handle circular references', () => { const obj: any = { a: 1, @@ -192,6 +208,7 @@ describe('objectPlain', () => { x: Symbol('x'), y: BigInt(123), fn, + bool: true, nan: Number.NaN, inf: Number.POSITIVE_INFINITY, err: new Error('err'), @@ -219,6 +236,8 @@ describe('objectPlain', () => { transformer(obj) { if (obj instanceof Date) return { __timestamp: obj.getTime() } + if (isBoolean(obj)) + return +obj }, }) @@ -229,6 +248,7 @@ describe('objectPlain', () => { "Klass": Object { "__class": "Function", }, + "bool": 1, "c": 2, "d": Array [ 3, diff --git a/src/common/dispose-defer.spec.ts b/src/common/dispose-defer.spec.ts index 92179ad2..39d11dcc 100644 --- a/src/common/dispose-defer.spec.ts +++ b/src/common/dispose-defer.spec.ts @@ -109,6 +109,19 @@ describe('dispose', () => { expect(stack).toEqual(['c', 'b', 'a']) }) + it('should dispose sync using using', async () => { + const stack: string[] = [] + function helper() { + using dispose = useDispose() + dispose.add(() => stack.push('a')) + dispose.add(() => stack.push('b')) + dispose.add(() => stack.push('c')) + expect(stack).toEqual([]) + } + helper() + expect(stack).toEqual(['c', 'b', 'a']) + }) + it('should dispose sync 2', async () => { const stack: string[] = [] const dispose = useDispose() @@ -124,25 +137,24 @@ describe('dispose', () => { }) // TODO future - // it("should use using", async () => { - // class TempFile implements Disposable { - - // constructor(path: string) { - // console.log('constructor') - // } - - // [Symbol.dispose]() { - // console.log('dispose') - // } - // } - - // function fn() { - // using f = new TempFile('abc') - // console.log('fn return') - // } - - // console.log('fn before') - // fn() - // console.log('fn after') - // }) + it('should use using', async () => { + class TempFile implements Disposable { + constructor(path: string) { + log('constructor') + } + + [Symbol.dispose]() { + log('dispose') + } + } + + function fn() { + using f = new TempFile('abc') + log('fn return') + } + + log('fn before') + fn() + log('fn after') + }) }) diff --git a/src/common/dispose-defer.ts b/src/common/dispose-defer.ts index 2373c1b0..901a3b7b 100644 --- a/src/common/dispose-defer.ts +++ b/src/common/dispose-defer.ts @@ -5,6 +5,16 @@ import { isPromise } from './exec/promise' import { DefaultLogger } from './log' import type { LoggerInterface } from './log/log-base' +// function polyfillUsing() { +// try { +// // @ts-expect-error just a polyfill +// Symbol.dispose ??= Symbol('Symbol.dispose') +// // @ts-expect-error just a polyfill +// Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose') +// } +// catch (err) { } +// } + /** Different kinds of implementations have grown, this should unify them */ function callDisposer(disposable: Disposer): Promise | void { let result @@ -116,6 +126,15 @@ export function useDispose(config?: string | UseDisposeConfig | LoggerInterface) isDisposed() { return tracked.length <= 0 }, + + [Symbol.dispose]() { + return dispose() + }, + + async [Symbol.asyncDispose]() { + return await dispose() + }, + }) } diff --git a/src/common/dispose-types.ts b/src/common/dispose-types.ts index 698b9ae2..602a278c 100644 --- a/src/common/dispose-types.ts +++ b/src/common/dispose-types.ts @@ -1,11 +1,17 @@ // https://blog.hediet.de/post/the_disposable_pattern_in_typescript // todo adopt for `using` https://www.totaltypescript.com/typescript-5-2-new-keyword-using +// export interface DisposerFunctionBase { +// (): any +// [Symbol.dispose]: () => void +// } + export type DisposerFunction = () => any | Promise /** @deprecated conflicts with `using` feature */ export type Disposer = DisposerFunction | { dispose: DisposerFunction // | Promise + // [Symbol.dispose]: // cleanup?: DisposerFunction | Promise // deprecated, but used often in my old code } diff --git a/src/common/dispose-utils.spec.ts b/src/common/dispose-utils.spec.ts index 0c4dfe6f..eab2573b 100644 --- a/src/common/dispose-utils.spec.ts +++ b/src/common/dispose-utils.spec.ts @@ -53,6 +53,26 @@ describe('useTimeout', () => { expect(emitter.off).toBeCalledWith(eventName, fn, ...args) }) + it('should add event listener using "on" method if available using using', () => { + const emitter = { + on: jest.fn(), + off: jest.fn(), + } + const eventName = 'click' + const fn = jest.fn() + const args = [1, 2, 3] + + function helper() { + using _ = useEventListener(emitter, eventName, fn, ...args) as any + expect(emitter.on).toBeCalledWith(eventName, fn, ...args) + expect(emitter.off).not.toBeCalled() + } + + helper() + + expect(emitter.off).toBeCalledWith(eventName, fn, ...args) + }) + it('should add event listener using "addEventListener" method if "on" method is not available', () => { const emitter = { addEventListener: jest.fn(), diff --git a/src/common/dispose-utils.ts b/src/common/dispose-utils.ts index ca98175d..f9602c3b 100644 --- a/src/common/dispose-utils.ts +++ b/src/common/dispose-utils.ts @@ -6,6 +6,12 @@ import type { LoggerInterface } from './log/log-base' export type TimerExecFunction = () => void | Promise +export const noopDisposer: () => DisposerFunction = () => { + const dispose = () => {} + dispose[Symbol.dispose] = dispose + return dispose +} + /** * Executes a function after a specified timeout and returns a disposer function * that can be used to cancel the timeout. @@ -16,12 +22,14 @@ export type TimerExecFunction = () => void | Promise */ export function useTimeout(fn: TimerExecFunction, timeout = 0): DisposerFunction { let timeoutHandle: any = setTimeout(fn, timeout) - return () => { + const dispose = () => { if (timeoutHandle) { clearTimeout(timeoutHandle) timeoutHandle = undefined } } + dispose[Symbol.dispose] = dispose + return dispose } /** @@ -34,12 +42,14 @@ export function useTimeout(fn: TimerExecFunction, timeout = 0): DisposerFunction */ export function useInterval(fn: TimerExecFunction, interval: number): DisposerFunction { let intervalHandle: any = setInterval(fn, interval) - return () => { + const dispose = () => { if (intervalHandle) { clearInterval(intervalHandle) intervalHandle = undefined } } + dispose[Symbol.dispose] = dispose + return dispose } /** The interval starts only, when the function is finished. */ @@ -56,13 +66,15 @@ export function useIntervalPause(fn: TimerExecFunction, interval: number, immedi void loop(immediately) - return () => { + const dispose = () => { if (intervalHandle) { stop = true clearInterval(intervalHandle) intervalHandle = undefined } } + dispose[Symbol.dispose] = dispose + return dispose } export function useEventListener( @@ -72,17 +84,19 @@ export function useEventListener( ...args: any[] ): DisposerFunction { if (emitter == null) - return () => { } + return noopDisposer() if (emitter.on) emitter.on(eventName, fn, ...args) else if (emitter.addEventListener) emitter.addEventListener(eventName, fn, ...args) - return () => { + const dispose = () => { if (emitter.off) emitter.off(eventName, fn, ...args) else if (emitter.removeEventListener) emitter.removeEventListener(eventName, fn, ...args) } + dispose[Symbol.dispose] = dispose + return dispose } export function useEventListenerOnce( @@ -92,17 +106,19 @@ export function useEventListenerOnce( ...args: any[] ): DisposerFunction { if (emitter == null) - return () => { } + return noopDisposer() if (emitter.on) emitter.once(eventName, fn, ...args) else if (emitter.addEventListener) emitter.addEventListener(eventName, fn, ...args) - return () => { + const dispose = () => { if (emitter.off) emitter.off(eventName, fn, ...args) else if (emitter.removeEventListener) emitter.removeEventListener(eventName, fn, ...args) } + dispose[Symbol.dispose] = dispose + return dispose } /** Like useDispose but with shorthands for emitter and timers */ diff --git a/src/node/files-async.ts b/src/node/files-async.ts index 96758ea8..15dec594 100644 --- a/src/node/files-async.ts +++ b/src/node/files-async.ts @@ -1,16 +1,43 @@ import { readdir, stat } from 'node:fs/promises' import { join, resolve } from 'node:path' import process from 'node:process' -import type { Stats } from 'node:fs' import { isHiddenPath } from './fs' import { globToRegExp } from './glob' +interface StatsBase { + isFile: () => boolean + isDirectory: () => boolean + isBlockDevice: () => boolean + isCharacterDevice: () => boolean + isSymbolicLink: () => boolean + isFIFO: () => boolean + isSocket: () => boolean + dev: number + ino: number + mode: number + nlink: number + uid: number + gid: number + rdev: number + size: number + blksize: number + blocks: number + atimeMs: number + mtimeMs: number + ctimeMs: number + birthtimeMs: number + atime: Date + mtime: Date + ctime: Date + birthtime: Date +} + /** * Retrieves the file system stats for the specified path asynchronously. * @param path - The path to the file or directory. * @returns A Promise that resolves to the file system stats (Stats) or undefined if an error occurs. */ -export async function getStatAsync(path: string): Promise { +export async function getStatAsync(path: string): Promise { try { return await stat(path) } diff --git a/src/node/fs.ts b/src/node/fs.ts index 5c8c5693..3038186b 100644 --- a/src/node/fs.ts +++ b/src/node/fs.ts @@ -47,7 +47,6 @@ export async function removeFolder(...parts: string[]): Promise { const path = joinPath(...parts) if (await exists(path)) await rm(path, { recursive: true }) - return path } diff --git a/tsconfig.json b/tsconfig.json index ff545aef..66f4a5bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "target": "ES2020", "jsx": "preserve", "lib": [ - "ESNext", + "ES2022", + "ESNext.Disposable", "DOM", "DOM.Iterable" ], diff --git a/vitest.config.ts b/vitest.config.ts index 890def65..c31e0aa8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,4 +18,7 @@ export default defineConfig({ '@/': `${resolve(process.cwd(), 'src')}/`, }, }, + + // https://github.com/vitest-dev/vitest/issues/4183 + esbuild: { target: 'es2022' }, }) From d565fad80c7825c61e123a7eeade3810822bfecf Mon Sep 17 00:00:00 2001 From: Dirk Holtwick Date: Fri, 21 Jun 2024 21:08:12 +0200 Subject: [PATCH 2/2] 0.20.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57bfa8a4..41acf5e0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zeed", "type": "module", - "version": "0.20.7", + "version": "0.20.8", "description": "🌱 Simple foundation library", "author": { "name": "Dirk Holtwick",