Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 17 additions & 4 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -22,7 +22,8 @@ export interface UseQueryOptions {

export interface QueryResult<TReturn> {
data: ComputedRef<HumanReadable<TReturn>>
status: ComputedRef<ResultType>
status: ComputedRef<QueryStatus>
error: ComputedRef<QueryError & { refetch: () => void } | undefined>
}

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

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

Expand All @@ -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,
),
}
}
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 QueryError = {
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<QueryError | 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<QueryError | 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): 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,
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