Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { createZeroComposables } from './create-zero-composables'
export type { QueryResult, UseQueryOptions } from './query'
export type { QueryError, QueryResult, UseQueryOptions } from './query'
export { useQuery, useQueryWithZero } from './query'
27 changes: 23 additions & 4 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// based on https://github.com/rocicorp/mono/tree/main/packages/zero-solid

import type { CustomMutatorDefs, HumanReadable, Query, ResultType, Schema, TTL, Zero } from '@rocicorp/zero'
import type { CustomMutatorDefs, HumanReadable, Query, Schema, TTL, Zero } from '@rocicorp/zero'
import type { ComputedRef, MaybeRefOrGetter } from 'vue'
import type { VueView } from './view'
import type { QueryErrorDetails, QueryStatus, VueView } from './view'

import {
computed,
getCurrentInstance,
onUnmounted,
ref,
shallowRef,
toValue,
watch,
Expand All @@ -20,9 +21,15 @@ export interface UseQueryOptions {
ttl?: TTL | undefined
}

export interface QueryError {
refetch: () => void
details: QueryErrorDetails
}

export interface QueryResult<TReturn> {
data: ComputedRef<HumanReadable<TReturn>>
status: ComputedRef<ResultType>
status: ComputedRef<QueryStatus>
error: ComputedRef<QueryError | undefined>
}

/**
Expand Down Expand Up @@ -60,9 +67,14 @@ export function useQueryWithZero<
return toValue(options)?.ttl ?? DEFAULT_TTL_MS
})
const view = shallowRef<VueView<HumanReadable<TReturn>> | null>(null)
const refetchKey = ref(0)

watch(
[() => toValue(query), () => toValue(z)],
[
() => toValue(query),
() => toValue(z),
refetchKey,
],
([q, z]) => {
view.value?.destroy()

Expand All @@ -88,5 +100,12 @@ export function useQueryWithZero<
return {
data: computed(() => view.value!.data),
status: computed(() => view.value!.status),
error: computed(() => view.value!.error
? {
refetch: () => { refetchKey.value++ },
details: view.value!.error,
}
: undefined,
),
}
}
90 changes: 68 additions & 22 deletions src/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,97 @@ import type {
Input,
Output,
Query,
ResultType,
ReadonlyJSONValue,
Schema,
TTL,
ViewFactory,
} from '@rocicorp/zero'
import type { Ref } from 'vue'
import { applyChange } from '@rocicorp/zero'
import { reactive } from 'vue'

interface QueryResultDetails {
readonly type: ResultType
import { ref } from 'vue'

// zero does not export this type
type ErroredQuery = {
error: 'app'
queryName: string
details: ReadonlyJSONValue
} | {
error: 'zero'
queryName: string
details: ReadonlyJSONValue
} | {
error: 'http'
queryName: string
status: number
details: ReadonlyJSONValue
}

type State = [Entry, QueryResultDetails]

const complete = { type: 'complete' } as const
const unknown = { type: 'unknown' } as const
export type QueryStatus = 'complete' | 'unknown' | 'error'

export type QueryErrorDetails = {
type: 'app'
queryName: string
details: ReadonlyJSONValue
} | {
type: 'http'
queryName: string
status: number
details: ReadonlyJSONValue
}

export class VueView<V> implements Output {
readonly #input: Input
readonly #format: Format
readonly #onDestroy: () => void
readonly #updateTTL: (ttl: TTL) => void

#state: State
#data: Ref<Entry>
#status: Ref<QueryStatus>
#error: Ref<QueryErrorDetails | undefined>

constructor(
input: Input,
onTransactionCommit: (cb: () => void) => void,
format: Format = { singular: false, relationships: {} },
format: Format,
onDestroy: () => void = () => {},
queryComplete: true | Promise<true>,
queryComplete: true | ErroredQuery | Promise<true>,
updateTTL: (ttl: TTL) => void,
) {
this.#input = input
this.#format = format
this.#onDestroy = onDestroy
this.#updateTTL = updateTTL
this.#state = reactive([
{ '': format.singular ? undefined : [] },
queryComplete === true ? complete : unknown,
])
this.#data = ref({ '': format.singular ? undefined : [] })
this.#status = ref(queryComplete === true ? 'complete' : 'error' in queryComplete ? 'error' : 'unknown')
this.#error = ref(queryComplete !== true && 'error' in queryComplete ? makeError(queryComplete) : undefined) as Ref<QueryErrorDetails | undefined>

input.setOutput(this)

for (const node of input.fetch({})) {
this.#applyChange({ type: 'add', node })
}

if (queryComplete !== true) {
if (queryComplete !== true && !('error' in queryComplete)) {
void queryComplete.then(() => {
this.#state[1] = complete
this.#status.value = 'complete'
this.#error.value = undefined
}).catch((error: ErroredQuery) => {
this.#status.value = 'error'
this.#error.value = makeError(error)
})
}
}

get data() {
return this.#state[0][''] as V
return this.#data.value[''] as V
}

get status() {
return this.#state[1].type
return this.#status.value
}

get error() {
return this.#error.value
}

destroy() {
Expand All @@ -76,7 +107,7 @@ export class VueView<V> implements Output {

#applyChange(change: Change): void {
applyChange(
this.#state[0],
this.#data.value,
change,
this.#input.getSchema(),
'',
Expand All @@ -93,6 +124,21 @@ export class VueView<V> implements Output {
}
}

function makeError(error: ErroredQuery): QueryErrorDetails {
return error.error === 'app' || error.error === 'zero'
? {
type: 'app',
queryName: error.queryName,
details: error.details,
}
: {
type: 'http',
queryName: error.queryName,
status: error.status,
details: error.details,
}
}

export function vueViewFactory<
TSchema extends Schema,
TTable extends keyof TSchema['tables'] & string,
Expand All @@ -103,7 +149,7 @@ export function vueViewFactory<
format: Format,
onDestroy: () => void,
onTransactionCommit: (cb: () => void) => void,
queryComplete: true | Promise<true>,
queryComplete: true | ErroredQuery | Promise<true>,
updateTTL?: (ttl: TTL) => void,
) {
interface UpdateTTL {
Expand Down