[READ-ONLY] Mirror of https://github.com/danielroe/zero-vue. Vue bindings for Zero
sync
vue
zero
1// based on https://github.com/rocicorp/mono/tree/main/packages/zero-solid
2
3import type {
4 AnyViewFactory,
5 Change,
6 Entry,
7 ErroredQuery,
8 Format,
9 Input,
10 Node,
11 Output,
12 Query,
13 QueryErrorDetails,
14 QueryResultDetails,
15 Schema,
16 Stream,
17 TTL,
18} from '@rocicorp/zero'
19import type { ViewChange } from '@rocicorp/zero/bindings'
20import type { Ref } from 'vue'
21import { applyChange, skipYields } from '@rocicorp/zero/bindings'
22import { ref } from 'vue'
23
24export type QueryStatus = QueryResultDetails['type']
25export type QueryError = QueryErrorDetails['error']
26
27export class VueView implements Output {
28 readonly #input: Input
29 readonly #format: Format
30 readonly #onDestroy: () => void
31 readonly #updateTTL: (ttl: TTL) => void
32
33 #data: Ref<Entry>
34 #status: Ref<QueryStatus>
35 #error: Ref<QueryError | undefined>
36 #isDestroyed = false
37
38 constructor(
39 input: Input,
40 onTransactionCommit: (cb: () => void) => void,
41 format: Format,
42 onDestroy: () => void = () => {},
43 queryComplete: true | ErroredQuery | Promise<true>,
44 updateTTL: (ttl: TTL) => void,
45 ) {
46 this.#input = input
47 this.#format = format
48 this.#onDestroy = onDestroy
49 this.#updateTTL = updateTTL
50 this.#data = ref({ '': format.singular ? undefined : [] })
51 this.#status = ref(queryComplete === true ? 'complete' : 'error' in queryComplete ? 'error' : 'unknown')
52 this.#error = ref(queryComplete !== true && 'error' in queryComplete ? makeError(queryComplete) : undefined) as Ref<QueryError | undefined>
53
54 input.setOutput(this)
55
56 for (const node of skipYields(input.fetch({}))) {
57 this.#applyChange({ type: 'add', node })
58 }
59
60 if (queryComplete !== true && !('error' in queryComplete)) {
61 void queryComplete.then(() => {
62 this.#status.value = 'complete'
63 this.#error.value = undefined
64 }).catch((error: ErroredQuery) => {
65 this.#status.value = 'error'
66 this.#error.value = makeError(error)
67 })
68 }
69 }
70
71 get data(): Entry[string] {
72 return this.#data.value['']
73 }
74
75 get status(): QueryStatus {
76 return this.#status.value
77 }
78
79 get error(): QueryError | undefined {
80 return this.#error.value
81 }
82
83 destroy(): void {
84 if (!this.#isDestroyed) {
85 this.#isDestroyed = true
86 this.#onDestroy()
87 }
88 }
89
90 #applyChange(change: ViewChange): void {
91 applyChange(
92 this.#data.value,
93 change,
94 this.#input.getSchema(),
95 '',
96 this.#format,
97 )
98 }
99
100 push(change: Change): readonly never[] {
101 this.#applyChange(materializeRelationships(change))
102 return Object.freeze([])
103 }
104
105 updateTTL(ttl: TTL): void {
106 this.#updateTTL(ttl)
107 }
108}
109
110function materializeRelationships(change: Change): ViewChange {
111 switch (change[0]) {
112 case 0:
113 return {
114 type: 'add',
115 node: materializeNodeRelationships(change[1]),
116 }
117 case 1:
118 return {
119 type: 'remove',
120 node: materializeNodeRelationships(change[1]),
121 }
122 case 2:
123 return {
124 type: 'edit',
125 node: { row: change[1].row },
126 oldNode: { row: change[2].row },
127 }
128 case 3:
129 return {
130 type: 'child',
131 node: { row: change[1].row },
132 child: {
133 relationshipName: change[2].relationshipName,
134 change: materializeRelationships(change[2].change),
135 },
136 }
137 }
138}
139
140function materializeNodeRelationships(node: Node): Node {
141 const relationships: Record<string, () => Stream<Node>> = {}
142
143 for (const relationship in node.relationships) {
144 const children = node.relationships[relationship]
145 if (!children) {
146 continue
147 }
148
149 const materialized: Node[] = []
150
151 for (const child of skipYields(children())) {
152 materialized.push(materializeNodeRelationships(child))
153 }
154
155 relationships[relationship] = () => materialized
156 }
157
158 return {
159 row: node.row,
160 relationships,
161 }
162}
163
164function makeError(error: ErroredQuery): QueryError {
165 const message = error.message ?? 'An unknown error occurred'
166 return {
167 type: error.error,
168 message,
169 ...(error.details ? { details: error.details } : {}),
170 }
171}
172
173export function vueViewFactory<
174 TTable extends keyof TSchema['tables'] & string,
175 TSchema extends Schema,
176 TReturn,
177>(
178 _query: Query<TTable, TSchema, TReturn>,
179 input: Input,
180 format: Format,
181 onDestroy: () => void,
182 onTransactionCommit: (cb: () => void) => void,
183 queryComplete: true | ErroredQuery | Promise<true>,
184 updateTTL: (ttl: TTL) => void,
185): VueView {
186 return new VueView(
187 input,
188 onTransactionCommit,
189 format,
190 onDestroy,
191 queryComplete,
192 updateTTL,
193 )
194}
195
196vueViewFactory satisfies AnyViewFactory