on-zero 0.4.39 → 0.4.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { useEmitterValue, type Emitter } from '@take-out/helpers'
8
8
  import { useContext, useMemo, useRef, useSyncExternalStore, type Context } from 'react'
9
9
 
10
10
  import {
11
+ emptyResponseFor,
11
12
  parseUseQueryArgs,
12
13
  type QueryControlMode,
13
14
  type UseQueryHook,
@@ -25,13 +26,10 @@ import type {
25
26
  // useQuery path via createZeroClient; this exists only for nested providers
26
27
  // where a non-innermost instance cannot be selected through react context.
27
28
 
28
- const EMPTY_RESPONSE = [null, { type: 'unknown' }] as never
29
+ // see createUseQuery.tsx empty responses must match the typed contract:
30
+ // plural queries get [], singular get undefined. returning null breaks the
31
+ // obvious .filter / .find / .length on first render.
29
32
  const DISABLED_SUBSCRIBE = () => () => {}
30
- const DISABLED_SNAPSHOT: readonly [undefined, { type: 'unknown' }] = [
31
- undefined,
32
- { type: 'unknown' },
33
- ]
34
- const getDisabledSnapshot = () => DISABLED_SNAPSHOT
35
33
 
36
34
  type DirectSnapshot = readonly [unknown, { type: string }]
37
35
 
@@ -255,16 +253,17 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
255
253
  getZero,
256
254
  zeroVersion,
257
255
  }: Parameters<CreateUseQueryDirect<Schema>>[0]): UseQueryHook<Schema> {
258
- // SSG: return an inert hook — see createUseQuery for the rationale.
256
+ // SSG: return an inert hook — see createUseQuery for the rationale. default
257
+ // to the plural empty shape (most queries are plural; a singular caller can
258
+ // tolerate [] better than the non-singular caller can tolerate null/undefined).
259
259
  if (typeof window === 'undefined') {
260
- return (() => EMPTY_RESPONSE) as UseQueryHook<Schema>
260
+ return ((_fn: any) => emptyResponseFor(undefined)) as UseQueryHook<Schema>
261
261
  }
262
262
 
263
263
  const directViewStore = new DirectViewStore()
264
264
 
265
265
  function useQueryDirect(...args: any[]): any {
266
266
  const disableMode = useContext(DisabledContext)
267
- const lastRef = useRef<any>(EMPTY_RESPONSE)
268
267
  const [fn, paramsOrOptions, optionsArg] = args
269
268
 
270
269
  const version = useEmitterValue(zeroVersion)
@@ -281,19 +280,32 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
281
280
 
282
281
  const paramsKey = params === undefined ? '' : JSON.stringify(params)
283
282
 
283
+ // resolve the query once so we know its singular/plural format up front —
284
+ // the no-zero / disabled snapshot needs to match that format so .filter /
285
+ // .find / .length is safe on first render.
286
+ const queryRequest = useMemo(
287
+ () => resolveQuery({ customQueries, fn, params }),
288
+ // params is keyed by paramsKey
289
+ // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ [fn, paramsKey],
291
+ )
292
+
293
+ const emptyForQuery = useMemo(() => emptyResponseFor(queryRequest), [queryRequest])
294
+ const lastRef = useRef<any>(emptyForQuery)
295
+
284
296
  const view = useMemo((): DirectView | null => {
285
297
  const zero = getZero()
286
298
  if (!zero) return null
287
- const queryRequest = resolveQuery({ customQueries, fn, params })
288
299
  return directViewStore.getView(zero, queryRequest, enabled, ttl)
289
- // params is keyed by paramsKey; version re-materializes on a new zero
300
+ // version re-materializes on a new zero
290
301
  // eslint-disable-next-line react-hooks/exhaustive-deps
291
- }, [fn, paramsKey, enabled, ttl, version])
302
+ }, [queryRequest, enabled, ttl, version])
292
303
 
304
+ const getEmpty = () => emptyForQuery as DirectSnapshot
293
305
  const out = useSyncExternalStore(
294
306
  view ? view.subscribe : DISABLED_SUBSCRIBE,
295
- view ? view.getSnapshot : getDisabledSnapshot,
296
- view ? view.getSnapshot : getDisabledSnapshot,
307
+ view ? view.getSnapshot : getEmpty,
308
+ view ? view.getSnapshot : getEmpty,
297
309
  )
298
310
 
299
311
  if (!disableMode) {
@@ -305,7 +317,7 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
305
317
  return lastRef.current
306
318
  }
307
319
 
308
- return EMPTY_RESPONSE
320
+ return emptyForQuery
309
321
  }
310
322
 
311
323
  return useQueryDirect as UseQueryHook<Schema>
@@ -0,0 +1,98 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // useQuery's typed contract is `[T[], info]` for plural queries. The empty /
4
+ // loading / disabled response MUST therefore be `[[], info]` — returning
5
+ // `[null, info]` (the old EMPTY_RESPONSE constant) broke any caller that does
6
+ // the obvious .filter / .find / .length / for-of on first render. Singular
7
+ // queries get `[undefined, info]` instead (the established zero-react shape).
8
+
9
+ import { createSchema, number, string, table } from '@rocicorp/zero'
10
+ import { act } from 'react'
11
+ import { createRoot, type Root } from 'react-dom/client'
12
+ import { afterEach, beforeEach, expect, test } from 'vitest'
13
+
14
+ import { createZeroClient } from './createZeroClient'
15
+ import { zql } from './zql'
16
+
17
+ declare global {
18
+ // eslint-disable-next-line no-var
19
+ var IS_REACT_ACT_ENVIRONMENT: boolean | undefined
20
+ }
21
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true
22
+
23
+ const todoTable = table('todo')
24
+ .columns({ id: string(), title: string(), createdAt: number() })
25
+ .primaryKey('id')
26
+ const schema = createSchema({ tables: [todoTable] })
27
+
28
+ // real plain query functions backed by zql so resolveQuery returns a real
29
+ // QueryRequest (asQueryInternals(...).format.singular works).
30
+ const allTodos = (_args: void) =>
31
+ (zql as unknown as { todo: { orderBy: (k: string, d: string) => any } }).todo.orderBy(
32
+ 'createdAt',
33
+ 'desc',
34
+ )
35
+ const oneTodo = (args: { id: string }) =>
36
+ (zql as unknown as { todo: { where: (k: string, v: string) => any } }).todo
37
+ .where('id', args.id)
38
+ .one()
39
+
40
+ const client = createZeroClient({
41
+ schema,
42
+ models: {},
43
+ groupedQueries: {
44
+ todo: { allTodos, oneTodo },
45
+ },
46
+ instanceName: 'empty-shape-test',
47
+ })
48
+
49
+ let container: HTMLDivElement
50
+ let root: Root
51
+
52
+ beforeEach(() => {
53
+ container = document.createElement('div')
54
+ root = createRoot(container)
55
+ })
56
+
57
+ afterEach(() => {
58
+ act(() => root.unmount())
59
+ })
60
+
61
+ function renderWithDisabled<T>(useHook: () => T): T {
62
+ let captured: T | undefined
63
+ const Probe = () => {
64
+ captured = useHook()
65
+ return null
66
+ }
67
+ act(() => {
68
+ // disable=true mounts the stable shell with DisabledContext='empty' + a
69
+ // stub Zero — this is exactly the path that used to return [null, ...]
70
+ // for every query.
71
+ root.render(
72
+ <client.ProvideZero authData={{}} userID="anon" disable>
73
+ <Probe />
74
+ </client.ProvideZero>,
75
+ )
76
+ })
77
+ if (captured === undefined) throw new Error('Probe did not render')
78
+ return captured
79
+ }
80
+
81
+ // regression: previously this returned [null, ...] which crashed downstream
82
+ // .filter / .find / .length. now it must return [[], ...] for plural queries.
83
+ test('useQuery returns [] (not null) for plural queries under DisabledContext', () => {
84
+ const [data, info] = renderWithDisabled(() => client.useQuery(allTodos))
85
+ expect(Array.isArray(data)).toBe(true)
86
+ expect((data as unknown[]).length).toBe(0)
87
+ // method calls that previously crashed must not crash
88
+ expect((data as unknown[]).filter(() => true)).toEqual([])
89
+ expect((data as unknown[]).find(() => true)).toBeUndefined()
90
+ expect(info?.type).toBe('unknown')
91
+ })
92
+
93
+ test('useQuery returns undefined (not null) for singular queries under DisabledContext', () => {
94
+ const [data, info] = renderWithDisabled(() => client.useQuery(oneTodo, { id: 'x' }))
95
+ // singular queries match zero-react: data is undefined while loading/disabled.
96
+ expect(data).toBeUndefined()
97
+ expect(info?.type).toBe('unknown')
98
+ })
@@ -14,6 +14,9 @@ export type UseQueryHook<Schema extends ZeroSchema> = {
14
14
  <TArg, TTable extends keyof Schema['tables'] & string, TReturn>(fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>, params: TArg, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
15
15
  <TTable extends keyof Schema['tables'] & string, TReturn>(fn: PlainQueryFn<void, Query<TTable, Schema, TReturn>>, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
16
16
  };
17
+ export declare function emptyResponseFor(queryRequest: unknown): readonly [unknown, {
18
+ type: string;
19
+ }];
17
20
  export declare function parseUseQueryArgs(paramsOrOptions: any, optionsArg: any): {
18
21
  params: any;
19
22
  options: any;
@@ -1 +1 @@
1
- {"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAGjE,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,KAAK,EACL,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAGvB,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,CAAA;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CAC9C,CAAA;AAED,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAA;AAExF,YAAY,EAAE,YAAY,EAAE,CAAA;AAE5B,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,UAAU,IAAI;IAEpD,CAAC,IAAI,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EAC5D,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,IAAI,EACZ,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAA;CACxB,CAAA;AAKD,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG;;;EAYtE;AAED,wBAAgB,cAAc,CAAC,MAAM,SAAS,UAAU,EAAE,EACxD,eAAe,EACf,aAAa,GACd,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;CAChC,GAAG,YAAY,CAAC,MAAM,CAAC,CAuDvB"}
1
+ {"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAE/D,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAGjE,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,KAAK,EACL,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAGvB,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,CAAA;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CAC9C,CAAA;AAED,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAA;AAExF,YAAY,EAAE,YAAY,EAAE,CAAA;AAE5B,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,UAAU,IAAI;IAEpD,CAAC,IAAI,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EAC5D,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,IAAI,EACZ,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAA;CACxB,CAAA;AAiBD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAU5F;AAGD,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG;;;EAYtE;AAED,wBAAgB,cAAc,CAAC,MAAM,SAAS,UAAU,EAAE,EACxD,eAAe,EACf,aAAa,GACd,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;CAChC,GAAG,YAAY,CAAC,MAAM,CAAC,CAgEvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"createUseQueryDirect.d.ts","sourceRoot":"","sources":["../src/createUseQueryDirect.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAqD,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAEvF,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,EACV,gBAAgB,EAEhB,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAqBvB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CACT,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,GACtB;QACD,WAAW,CACT,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,gBAAgB,KAAK,IAAI,GACpE,IAAI,CAAA;QACP,OAAO,IAAI,IAAI,CAAA;QACf,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAA;KAC1B,CAAA;CACF,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,MAAM,SAAS,UAAU,IAAI,CAAC,KAAK,EAAE;IACpE,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;IAC/B,OAAO,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IACxC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7B,KAAK,YAAY,CAAC,MAAM,CAAC,CAAA;AAG1B,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAsLD,wBAAgB,oBAAoB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC9D,eAAe,EACf,aAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE,UAAU,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAuDpE"}
1
+ {"version":3,"file":"createUseQueryDirect.d.ts","sourceRoot":"","sources":["../src/createUseQueryDirect.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAqD,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAEvF,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,EACV,gBAAgB,EAEhB,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAkBvB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CACT,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,GACtB;QACD,WAAW,CACT,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,gBAAgB,KAAK,IAAI,GACpE,IAAI,CAAA;QACP,OAAO,IAAI,IAAI,CAAA;QACf,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAA;KAC1B,CAAA;CACF,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,MAAM,SAAS,UAAU,IAAI,CAAC,KAAK,EAAE;IACpE,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;IAC/B,OAAO,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IACxC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7B,KAAK,YAAY,CAAC,MAAM,CAAC,CAAA;AAG1B,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAsLD,wBAAgB,oBAAoB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC9D,eAAe,EACf,aAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE,UAAU,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAqEpE"}
@@ -0,0 +1,5 @@
1
+ declare global {
2
+ var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
3
+ }
4
+ export {};
5
+ //# sourceMappingURL=useQuery.empty.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useQuery.empty.test.d.ts","sourceRoot":"","sources":["../src/useQuery.empty.test.tsx"],"names":[],"mappings":"AAgBA,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,wBAAwB,EAAE,OAAO,GAAG,SAAS,CAAA;CAClD"}