Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(until): improved until types #1493

Merged
merged 6 commits into from Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -39,6 +39,7 @@
"@iconify/json": "^2.1.35",
"@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((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