diff --git a/src/query.ts b/src/query.ts index 116abcd..ee39082 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,8 +1,8 @@ // 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 { QueryError, QueryStatus, VueView } from './view' import { computed, @@ -22,7 +22,8 @@ export interface UseQueryOptions { export interface QueryResult { data: ComputedRef> - status: ComputedRef + status: ComputedRef + error: ComputedRef void } | undefined> } /** @@ -60,9 +61,14 @@ export function useQueryWithZero< return toValue(options)?.ttl ?? DEFAULT_TTL_MS }) const view = shallowRef> | null>(null) + const refetchKey = shallowRef(0) watch( - [() => toValue(query), () => toValue(z)], + [ + () => toValue(query), + () => toValue(z), + refetchKey, + ], ([q, z]) => { view.value?.destroy() @@ -88,5 +94,12 @@ export function useQueryWithZero< return { data: computed(() => view.value!.data), status: computed(() => view.value!.status), + error: computed(() => view.value!.error + ? { + refetch: () => { refetchKey.value++ }, + ...view.value!.error, + } + : undefined, + ), } } diff --git a/src/view.ts b/src/view.ts index 132d88f..4bcd59b 100644 --- a/src/view.ts +++ b/src/view.ts @@ -8,22 +8,43 @@ 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 QueryError = { + type: 'app' + queryName: string + details: ReadonlyJSONValue +} | { + type: 'http' + queryName: string + status: number + details: ReadonlyJSONValue +} export class VueView implements Output { readonly #input: Input @@ -31,43 +52,53 @@ export class VueView implements Output { readonly #onDestroy: () => void readonly #updateTTL: (ttl: TTL) => void - #state: State + #data: Ref + #status: Ref + #error: Ref constructor( input: Input, onTransactionCommit: (cb: () => void) => void, - format: Format = { singular: false, relationships: {} }, + format: Format, onDestroy: () => void = () => {}, - queryComplete: true | Promise, + queryComplete: true | ErroredQuery | Promise, 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 + 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() { @@ -76,7 +107,7 @@ export class VueView implements Output { #applyChange(change: Change): void { applyChange( - this.#state[0], + this.#data.value, change, this.#input.getSchema(), '', @@ -93,6 +124,21 @@ export class VueView implements Output { } } +function makeError(error: ErroredQuery): QueryError { + 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, @@ -103,7 +149,7 @@ export function vueViewFactory< format: Format, onDestroy: () => void, onTransactionCommit: (cb: () => void) => void, - queryComplete: true | Promise, + queryComplete: true | ErroredQuery | Promise, updateTTL?: (ttl: TTL) => void, ) { interface UpdateTTL {