Skip to content

Commit

Permalink
Merge branch 'release/v0.17.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
holtwick committed Mar 13, 2024
2 parents b4a059f + 3318446 commit df46343
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 9 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zeed",
"type": "module",
"version": "0.17.1",
"version": "0.17.2",
"description": "🌱 Simple foundation library",
"author": {
"name": "Dirk Holtwick",
Expand Down Expand Up @@ -65,9 +65,9 @@
"watch": "nr build -- --watch"
},
"devDependencies": {
"@antfu/eslint-config": "^2.8.0",
"@antfu/eslint-config": "^2.8.1",
"@antfu/ni": "^0.21.12",
"@types/node": "^20.11.25",
"@types/node": "^20.11.26",
"@vitest/coverage-v8": "^1.3.1",
"esbuild": "^0.20.1",
"eslint": "^8.57.0",
Expand Down
189 changes: 189 additions & 0 deletions src/common/dispose-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { vi as jest } from 'vitest'
import { useDisposeWithUtils, useEventListener, useTimeout } from './dispose-utils'

describe('useTimeout', () => {
it('should call the provided function after the specified timeout', () => {
jest.useFakeTimers()

const mockFn = jest.fn()
const timeout = 1000

useTimeout(mockFn, timeout)

expect(mockFn).not.toBeCalled()

jest.advanceTimersByTime(timeout)

expect(mockFn).toBeCalled()
})

it('should cancel the timeout when the disposer function is called', () => {
jest.useFakeTimers()

const mockFn = jest.fn()
const timeout = 1000

const disposer = useTimeout(mockFn, timeout)

expect(mockFn).not.toBeCalled()

disposer()

jest.advanceTimersByTime(timeout)

expect(mockFn).not.toBeCalled()
})

it('should add event listener using "on" method if available', () => {
const emitter = {
on: jest.fn(),
off: jest.fn(),
}
const eventName = 'click'
const fn = jest.fn()
const args = [1, 2, 3]

const disposer = useEventListener(emitter, eventName, fn, ...args)

expect(emitter.on).toBeCalledWith(eventName, fn, ...args)
expect(emitter.off).not.toBeCalled()

disposer()

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(),
removeEventListener: jest.fn(),
}
const eventName = 'click'
const fn = jest.fn()
const args = [1, 2, 3]

const disposer = useEventListener(emitter, eventName, fn, ...args)

expect(emitter.addEventListener).toBeCalledWith(eventName, fn, ...args)
expect(emitter.removeEventListener).not.toBeCalled()

disposer()

expect(emitter.removeEventListener).toBeCalledWith(eventName, fn, ...args)
})

it('should return an empty disposer function if emitter is null', () => {
const emitter = null
const eventName = 'click'
const fn = jest.fn()
const args = [1, 2, 3]

const disposer = useEventListener(emitter, eventName, fn, ...args)

expect(disposer).toEqual(expect.any(Function))

disposer() // Should not throw any error
})

it('should add timeout disposer to the dispose object', () => {
jest.useFakeTimers()

const mockFn = jest.fn()
const timeout = 1000

const dispose = useDisposeWithUtils()
dispose.timeout(mockFn, timeout)

expect(mockFn).not.toBeCalled()

jest.advanceTimersByTime(timeout)

expect(mockFn).toBeCalled()
})

it('should add interval disposer to the dispose object', () => {
jest.useFakeTimers()

const mockFn = jest.fn()
const interval = 1000

const dispose = useDisposeWithUtils()
dispose.interval(mockFn, interval)

expect(mockFn).not.toBeCalled()

jest.advanceTimersByTime(interval)

expect(mockFn).toBeCalled()
})

// it('should add interval pause disposer to the dispose object', () => {
// jest.useFakeTimers()

// const mockFn = jest.fn()
// const interval = 1000

// const dispose = useDisposeWithUtils()
// dispose.intervalPause(mockFn, interval)

// expect(mockFn).not.toBeCalled()

// jest.advanceTimersByTime(interval)

// expect(mockFn).not.toBeCalled()
// })

it('should add event listener disposer to the dispose object using "on" method', async () => {
const emitter = {
on: jest.fn(),
off: jest.fn(),
}
const eventName = 'click'
const fn = jest.fn()
const args = [1, 2, 3]

const dispose = useDisposeWithUtils()
dispose.on(emitter, eventName, fn, ...args)

expect(emitter.on).toBeCalledWith(eventName, fn, ...args)
expect(emitter.off).not.toBeCalled()

await dispose.dispose()

expect(emitter.off).toBeCalledWith(eventName, fn, ...args)
})

it('should add event listener disposer to the dispose object using "addEventListener" method', async () => {
const emitter = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
}
const eventName = 'click'
const fn = jest.fn()
const args = [1, 2, 3]

const dispose = useDisposeWithUtils()
dispose.on(emitter, eventName, fn, ...args)

expect(emitter.addEventListener).toBeCalledWith(eventName, fn, ...args)
expect(emitter.removeEventListener).not.toBeCalled()

await dispose.dispose()

expect(emitter.removeEventListener).toBeCalledWith(eventName, fn, ...args)
})

it('should return an empty disposer function if emitter is null 2', () => {
const emitter = null
const eventName = 'click'
const fn = jest.fn()
const args = [1, 2, 3]

const dispose = useDisposeWithUtils()
const disposer = dispose.on(emitter, eventName, fn, ...args)

expect(disposer).toEqual(expect.any(Function))

disposer!() // Should not throw any error
})
})
16 changes: 16 additions & 0 deletions src/common/dispose-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import type { LoggerInterface } from './log/log-base'

export type TimerExecFunction = () => void | Promise<void>

/**
* Executes a function after a specified timeout and returns a disposer function
* that can be used to cancel the timeout.
*
* @param fn - The function to execute after the timeout.
* @param timeout - The timeout duration in milliseconds (default: 0).
* @returns A disposer function that can be used to cancel the timeout.
*/
export function useTimeout(fn: TimerExecFunction, timeout = 0): DisposerFunction {
let timeoutHandle: any = setTimeout(fn, timeout)
return () => {
Expand All @@ -16,6 +24,14 @@ export function useTimeout(fn: TimerExecFunction, timeout = 0): DisposerFunction
}
}

/**
* Executes a function repeatedly at a specified interval and returns a disposer function
* that can be used to stop the execution.
*
* @param fn - The function to be executed at the specified interval.
* @param interval - The interval (in milliseconds) at which the function should be executed.
* @returns A disposer function that can be used to stop the execution of the function.
*/
export function useInterval(fn: TimerExecFunction, interval: number): DisposerFunction {
let intervalHandle: any = setInterval(fn, interval)
return () => {
Expand Down
8 changes: 8 additions & 0 deletions src/common/msg/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,21 @@ export class Emitter<

_logEmitter = DefaultLogger('zeed:emitter', 'warn')

/** RPC like emitting of events. */
call: RemoteListener = new Proxy<RemoteListener>({} as any, {
get:
(target: any, name: any) =>
async (...args: any): Promise<boolean> =>
await this.emit(name, ...args),
})

/**
* Emits an event to all subscribers and executes their corresponding event handlers.
*
* @param event - The event to emit.
* @param args - The arguments to pass to the event handlers.
* @returns A promise that resolves to a boolean indicating whether the event was successfully emitted.
*/
public async emit<U extends keyof RemoteListener>(event: U, ...args: Parameters<RemoteListener[U]>): Promise<boolean> {
let ok = false

Expand Down
2 changes: 1 addition & 1 deletion src/common/network.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchText, parseBasicAuth } from './network'
import { fetchJson, fetchText, parseBasicAuth } from './network'

describe('network', () => {
it('should fetch', async () => {
Expand Down
11 changes: 10 additions & 1 deletion src/common/platform.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-new-func */

import { detect } from './platform'
import { detect, getGlobal } from './platform'

describe('platform', () => {
it('should detect', () => {
Expand All @@ -25,4 +25,13 @@ describe('platform', () => {
expect(isBrowser()).toBe(true)
}
})

it('should return the global object', () => {
const globalObject = getGlobal()

if (globalThis.isNodeTestEnv)
expect(globalObject).toBe(globalThis)
else
expect(globalObject).toBe(window)
})
})
12 changes: 12 additions & 0 deletions src/common/platform.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable node/prefer-global/process */

/**
* Retrieves the global `window` object.
* @returns The global `window` object if available, otherwise `undefined`.
*/
export function getWindow(): any | undefined {
if (typeof window !== 'undefined')
return window
}

/**
* Retrieves the navigator object if it is available.
* @returns The navigator object if available, otherwise undefined.
*/
export function getNavigator(): any | undefined {
if (typeof navigator !== 'undefined')
return navigator
}

/**
* Retrieves the global object in the current environment.
* @returns The global object.
*/
export function getGlobal(): any {
return getWindow()
// @ts-expect-error xxx
Expand Down
20 changes: 19 additions & 1 deletion src/common/time.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sleep } from './exec'
import { duration } from './time'
import { duration, parseDate } from './time'

describe('time.spec', () => {
it('should measure', async () => {
Expand All @@ -9,4 +9,22 @@ describe('time.spec', () => {
// console.log(`elapsed time: ${elapsed}`)
expect(/\d+.\d\d ms/.test(elapsed)).toBe(true)
})

it('should parse valid date strings', () => {
const date1 = parseDate('2022-01-01')
const date2 = parseDate('2022-01-01T12:00:00')
expect(date1).toEqual(new Date(2022, 0, 1, 12, 0))
expect(date2).toEqual(new Date('2022-01-01T12:00:00'))
})

it('should return undefined for invalid date strings', () => {
const date = parseDate('invalid-date')
expect(date).toBeUndefined()
})

it('should return the input date if it is already a Date object', () => {
const inputDate = new Date()
const date = parseDate(inputDate)
expect(date).toBe(inputDate)
})
})
6 changes: 6 additions & 0 deletions src/common/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export function formatMilliseconds(ms: number): string {
return ms > 999 ? `${(ms / 1000).toFixed(1)} s` : `${ms.toFixed(2)} ms`
}

/**
* Parses the given date candidates and returns the first valid Date object found.
*
* @param dateCandidates - The date candidates to parse, which can be either strings or Date objects.
* @returns The parsed Date object, or undefined if no valid date is found.
*/
export function parseDate(
...dateCandidates: (string | Date)[]
): Date | undefined {
Expand Down
9 changes: 9 additions & 0 deletions src/node/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import process from 'node:process'
import { toCamelCase } from '../common/data/camelcase'

/**
* Represents the configuration options for parsing command line arguments.
*/
interface ParseConfig {
args?: string[]
alias?: Record<string, string[]>
Expand All @@ -12,6 +15,12 @@ interface ParseConfig {
numberArgs?: string | string[]
}

/**
* Parses command line arguments and returns an object containing the parsed options.
*
* @param config - Configuration options for parsing the arguments.
* @returns An object containing the parsed options.
*/
export function parseArgs(config: ParseConfig = {}) {
const {
args = process.argv.slice(1),
Expand Down
Loading

0 comments on commit df46343

Please sign in to comment.