Skip to content

Commit

Permalink
feat(until): improved until types (#1493)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
zojize and antfu committed Jun 16, 2022
1 parent d257715 commit c48533f
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 38 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -39,6 +39,7 @@
"@iconify/json": "^2.1.62",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-replace": "^4.0.0",
"@type-challenges/utils": "^0.1.1",
"@types/fs-extra": "^9.0.13",
"@types/js-yaml": "^4.0.5",
"@types/md5": "^2.3.2",
Expand Down
87 changes: 75 additions & 12 deletions packages/shared/until/index.test.ts
@@ -1,5 +1,7 @@
import type { Ref } from 'vue-demi'
import { ref } from 'vue-demi'
import { invoke } from '@vueuse/shared'
import type { Equal, Expect } from '@type-challenges/utils'
import { until } from '.'

describe('until', () => {
Expand All @@ -9,8 +11,8 @@ describe('until', () => {

invoke(async () => {
expect(r.value).toBe(0)
await until(r).toBe(1)
expect(r.value).toBe(1)
const x = await until(r).toBe(1)
expect(x).toBe(1)
resolve()
}).catch(reject)

Expand All @@ -26,8 +28,8 @@ describe('until', () => {

invoke(async () => {
expect(r.value).toBe(0)
await until(r).changedTimes(3)
expect(r.value).toBe(3)
const x = await until(r).changedTimes(3)
expect(x).toBe(3)
resolve()
}).catch(reject)

Expand All @@ -45,8 +47,8 @@ describe('until', () => {

invoke(async () => {
expect(r.value).toBe(0)
await until(r).not.toBe(0)
expect(r.value).toBe(1)
const x = await until(r).not.toBe(0)
expect(x).toBe(1)
resolve()
}).catch(reject)

Expand All @@ -62,8 +64,8 @@ describe('until', () => {

invoke(async () => {
expect(r.value).toBe(null)
await until(r).not.toBeNull()
expect(r.value).toBe(1)
const x = await until(r).not.toBeNull()
expect(x).toBe(1)
resolve()
}).catch(reject)

Expand All @@ -79,8 +81,8 @@ describe('until', () => {

invoke(async () => {
expect(r.value).toEqual([1, 2, 3])
await until(r).toContains(4, { deep: true })
expect(r.value).toEqual([1, 2, 3, 4])
const x = await until(r).toContains(4, { deep: true })
expect(x).toEqual([1, 2, 3, 4])
resolve()
}).catch(reject)

Expand All @@ -96,8 +98,8 @@ describe('until', () => {

invoke(async () => {
expect(r.value).toEqual([1, 2, 3])
await until(r).not.toContains(2, { deep: true })
expect(r.value).toEqual([1])
const x = await until(r).not.toContains(2, { deep: true })
expect(x).toEqual([1])
resolve()
}).catch(reject)

Expand All @@ -107,4 +109,65 @@ describe('until', () => {
}, 100)
})
})

it('should immediately timeout', () => {
return new Promise<void>((resolve, reject) => {
const r = ref(0)

invoke(async () => {
expect(r.value).toBe(0)
const x = await until(r).toBe(1, { timeout: 0 })
expect(x).toBe(0)
resolve()
}).catch(reject)

setTimeout(() => {
r.value = 1
}, 100)
})
})

it('should type check', () => {
async () => {
const x = ref<'x'>()
// type checks are done this way to prevent unused variable warnings
// and duplicate name warnings
'test' as any as Expect<Equal<typeof x, Ref<'x' | undefined>>>

const one = await until(x).toBe(1 as const)
'test' as any as Expect<Equal<typeof one, 1>>

const xTruthy = await until(x).toBeTruthy()
'test' as any as Expect<Equal<typeof xTruthy, 'x'>>

const xFalsy = await until(x).not.toBeTruthy()
'test' as any as Expect<Equal<typeof xFalsy, undefined>>

const xUndef = await until(x).toBeUndefined()
'test' as any as Expect<Equal<typeof xUndef, undefined>>

const xNotUndef = await until(x).not.toBeUndefined()
'test' as any as Expect<Equal<typeof xNotUndef, 'x'>>

const y = ref<'y' | null>(null)
'test' as any as Expect<Equal<typeof y, Ref<'y' | null>>>

const yNull = await until(y).toBeNull()
'test' as any as Expect<Equal<typeof yNull, null>>

const yNotNull = await until(y).not.toBeNull()
'test' as any as Expect<Equal<typeof yNotNull, 'y'>>

const z = ref<1 | 2 | 3>(1)
'test' as any as Expect<Equal<typeof z, Ref<1 | 2 | 3>>>

const is1 = (x: number): x is 1 => x === 1

const z1 = await until(z).toMatch(is1)
'test' as any as Expect<Equal<typeof z1, 1>>

const zNot1 = await until(z).not.toMatch(is1)
'test' as any as Expect<Equal<typeof zNot1, 2 | 3>>
}
})
})
92 changes: 66 additions & 26 deletions packages/shared/until/index.ts
@@ -1,5 +1,5 @@
import type { WatchOptions, WatchSource } from 'vue-demi'
import { unref, watch } from 'vue-demi'
import { isRef, unref, watch } from 'vue-demi'
import type { ElementOf, MaybeRef, ShallowUnwrapRef } from '../utils'
import { promiseTimeout } from '../utils'

Expand Down Expand Up @@ -34,29 +34,35 @@ export interface UntilToMatchOptions {
deep?: WatchOptions['deep']
}

export interface UntilBaseInstance<T> {
export interface UntilBaseInstance<T, Not extends boolean = false> {
toMatch<U extends T = T>(
condition: (v: T) => v is U,
options?: UntilToMatchOptions
): Not extends true ? Promise<Exclude<T, U>> : Promise<U>
toMatch(
condition: (v: T) => boolean,
options?: UntilToMatchOptions
): Promise<void>
changed(options?: UntilToMatchOptions): Promise<void>
changedTimes(n?: number, options?: UntilToMatchOptions): Promise<void>
): Promise<T>
changed(options?: UntilToMatchOptions): Promise<T>
changedTimes(n?: number, options?: UntilToMatchOptions): Promise<T>
}

export interface UntilValueInstance<T> extends UntilBaseInstance<T> {
readonly not: UntilValueInstance<T>
type Falsy = false | void | null | undefined | 0 | 0n | ''

export interface UntilValueInstance<T, Not extends boolean = false> extends UntilBaseInstance<T, Not> {
readonly not: UntilValueInstance<T, Not extends true ? false : true>

toBe<P = T>(value: MaybeRef<T | P>, options?: UntilToMatchOptions): Promise<void>
toBeTruthy(options?: UntilToMatchOptions): Promise<void>
toBeNull(options?: UntilToMatchOptions): Promise<void>
toBeUndefined(options?: UntilToMatchOptions): Promise<void>
toBeNaN(options?: UntilToMatchOptions): Promise<void>
toBe<P = T>(value: MaybeRef<P>, options?: UntilToMatchOptions): Not extends true ? Promise<T> : Promise<P>
toBeTruthy(options?: UntilToMatchOptions): Not extends true ? Promise<T & Falsy> : Promise<Exclude<T, Falsy>>
toBeNull(options?: UntilToMatchOptions): Not extends true ? Promise<Exclude<T, null>> : Promise<null>
toBeUndefined(options?: UntilToMatchOptions): Not extends true ? Promise<Exclude<T, undefined>> : Promise<undefined>
toBeNaN(options?: UntilToMatchOptions): Promise<T>
}

export interface UntilArrayInstance<T> extends UntilBaseInstance<T> {
readonly not: UntilArrayInstance<T>

toContains(value: MaybeRef<ElementOf<ShallowUnwrapRef<T>>>, options?: UntilToMatchOptions): Promise<void>
toContains(value: MaybeRef<ElementOf<ShallowUnwrapRef<T>>>, options?: UntilToMatchOptions): Promise<T>
}

/**
Expand All @@ -80,15 +86,15 @@ export function until<T>(r: any): any {
function toMatch(
condition: (v: any) => boolean,
{ flush = 'sync', deep = false, timeout, throwOnTimeout }: UntilToMatchOptions = {},
): Promise<void> {
): Promise<T> {
let stop: Function | null = null
const watcher = new Promise<void>((resolve) => {
const watcher = new Promise<T>((resolve) => {
stop = watch(
r,
(v) => {
if (condition(v) === !isNot) {
if (condition(v) !== isNot) {
stop?.()
resolve()
resolve(v)
}
},
{
Expand All @@ -100,19 +106,53 @@ export function until<T>(r: any): any {
})

const promises = [watcher]
if (timeout) {
if (timeout != null) {
promises.push(
promiseTimeout(timeout, throwOnTimeout).finally(() => {
stop?.()
}),
promiseTimeout(timeout, throwOnTimeout)
.then(() => unref(r))
.finally(() => stop?.()),
)
}

return Promise.race(promises)
}

function toBe<P>(value: MaybeRef<P | T>, options?: UntilToMatchOptions) {
return toMatch(v => v === unref(value), options)
if (!isRef(value))
return toMatch(v => v === value, options)

const { flush = 'sync', deep = false, timeout, throwOnTimeout } = options ?? {}
let stop: Function | null = null
const watcher = new Promise<T>((resolve) => {
stop = watch(
[r, value],
([v1, v2]) => {
if (isNot !== (v1 === v2)) {
stop?.()
resolve(v1)
}
},
{
flush,
deep,
immediate: true,
},
)
})

const promises = [watcher]
if (timeout != null) {
promises.push(
promiseTimeout(timeout, throwOnTimeout)
.then(() => unref(r))
.finally(() => {
stop?.()
return unref(r)
}),
)
}

return Promise.race(promises)
}

function toBeTruthy(options?: UntilToMatchOptions) {
Expand Down Expand Up @@ -167,13 +207,13 @@ export function until<T>(r: any): any {
return instance
}
else {
const instance: UntilValueInstance<T> = {
const instance: UntilValueInstance<T, boolean> = {
toMatch,
toBe,
toBeTruthy,
toBeNull,
toBeTruthy: toBeTruthy as any,
toBeNull: toBeNull as any,
toBeNaN,
toBeUndefined,
toBeUndefined: toBeUndefined as any,
changed,
changedTimes,
get not() {
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c48533f

Please sign in to comment.