Skip to content

feat: react-query integration#37

Merged
Nr9 merged 9 commits intomainfrom
feat/react-query
Aug 16, 2025
Merged

feat: react-query integration#37
Nr9 merged 9 commits intomainfrom
feat/react-query

Conversation

@Nr9
Copy link
Copy Markdown

@Nr9 Nr9 commented Aug 15, 2025

Summary by CodeRabbit

  • New Features

    • Added a Firestore snapshot lifecycle manager, factory-driven snapshot query helpers, schema-aware utilities, and Sentry listeners.
  • Documentation

    • Revamped root README with badges and per-package links; added package READMEs and consolidated install/dev commands and contributing guidance.
  • Tests

    • Large suite of Vitest tests added across packages with strict coverage configs and multi-project test setup.
  • Chores

    • Tooling and manifest updates: Vitest configs, ESLint adjustments, dependency/peerDependency tweaks, and workspace/release config changes.
  • Bug Fixes

    • Fixed broken pnpm link and improved installation guidance.

@Nr9 Nr9 requested a review from a team as a code owner August 15, 2025 23:14
@Nr9 Nr9 requested a review from fpbouchard August 15, 2025 23:14
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 15, 2025

Walkthrough

Adds a FirestoreSnapshotManager with subject factories, refactors React Query option builders to use factory-produced queryFns, restructures rxjs/sentry subject types and exports, introduces many Vitest test suites and configs, and updates package manifests, READMEs, and dev tooling.

Changes

Cohort / File(s) Summary
Project README & docs
README.md, packages/react-kitchen-sink/README.md, packages/zustand-firestore/README.md
Updated project description, badges, package table, install/dev notes, contributing steps, and per-package README additions.
Root manifests & workspace
package.json, pnpm-workspace.yaml, nx.json, vitest.config.mjs
Added devDeps (e.g., @nx/eslint, jsdom), pnpm workspace keys, nx release/plugins configuration, removed defaultBase, and added top-level Vitest multi-project config.
Top-level package scripts/deps
packages/*/package.json (react-kitchen-sink, react-firestore, zustand-firestore, rxjs-firebase)
Moved/added dependencies between dependencies/devDependencies/peerDependencies, added test scripts, adjusted ESLint script placement.
New manager & React Query integration
packages/react-kitchen-sink/src/react-query/FirestoreSnapshotManager.ts, .../FirestoreSnaphotManager.ts, .../index.ts, .../documentSnapshotQueryOptions.ts, .../querySnapshotQueryOptions.ts, .../schemaQuerySnapshotQueryOptions.ts
Added FirestoreSnapshotManager class, renamed manager type, introduced registerSnapshotOnClose and multiple subject factory members, and refactored option builders to factory-based queryFns with TError/TData generics and Result types.
QueryFn factories (new)
packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromDocumentSnapshotSubjectFactory.ts, .../queryFnFromQuerySnapshotSubjectFactory.ts
New factories producing React Query QueryFunctions from snapshot subject factories; implement waitForData, timeouts, abort handling, and QueryClient updates.
Snapshot subjects & rxjs layer
packages/react-kitchen-sink/src/rxjs/* (documentSnapshotSubject.ts, schemaDocumentSnapshotSubject.ts, schemaQuerySnapshotSubject.ts, types.ts, index.ts)
Added schema subject helpers and types, adjusted listener import paths, explicit generics, new re-exports and subject aliases, and moved some types to .../types.ts.
Observers & client wiring
packages/react-kitchen-sink/src/react-query/observers/*, .../documentSnapshotQueryClientObserver.ts, .../querySnapshotQueryClientObserver.ts
Added/updated observers and tests validating next/error/complete flows and QueryClient setQueryData interactions (Sentry capture mocked).
Sentry utilities & exports
packages/react-kitchen-sink/src/sentry/*, .../index.ts
Added sentryDocumentSnapshotListener and sentrySchemaQuerySnapshotListener, tests for listeners and Sentry-related error/context utilities, and re-exported new modules.
Schema & converters
packages/react-kitchen-sink/src/schema/*
Added generics to firestoreDataConverter, added Zod/schema tests validating conversions and Zod error behavior.
Zustand package updates & tests
packages/zustand-firestore/*
Moved deps to peer/dev, added Vitest config and many tests for stores/hooks, switched type imports to @firebase/firestore.
RxJS-Firebase minor edits
packages/rxjs-firebase/src/subjects/QuerySnapshotSubject.ts, packages/rxjs-firebase/package.json
Renamed constructor parameter to listener and adjusted ESLint script ordering.
Build / lint / test configs
packages/react-kitchen-sink/rollup.config.js, packages/react-kitchen-sink/eslint.config.mjs, packages/*/vitest.config.mjs
Added type-fest to rollup externals, replaced Jest config with Vitest blocks, added strict coverage thresholds and package-level Vitest configs.
Extensive tests added
packages/react-kitchen-sink/src/**/__tests__/*, packages/zustand-firestore/src/**/__tests__/*, packages/rxjs-firebase/src/**/__tests__/*
Many new Vitest suites covering manager lifecycle, option builders, queryFns, rxjs subjects, sentry utilities, schema conversions, and zustand store/hooks.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Component/Hook
  participant RQ as React Query
  participant FSM as FirestoreSnapshotManager
  participant Sub as SnapshotSubject
  participant FS as Firestore

  UI->>RQ: useQuery with snapshot options
  RQ->>FSM: request subject factory (ref/query/factory, snapshotOptions, listener)
  FSM-->>RQ: returns (queryKey) => Sub
  RQ->>Sub: instantiate via factory(queryKey)
  Sub->>FS: attach onSnapshot
  Sub-->>RQ: emit SnapshotState via observer
  RQ-->>UI: deliver state update
Loading
sequenceDiagram
  participant RQ as React Query Cache
  participant FSM as FirestoreSnapshotManager
  participant Sub as SnapshotSubject
  participant Sentry as Sentry

  RQ-->>FSM: notify removed { meta.type: 'snapshot', queryKey }
  FSM->>FSM: lookup onClose callback by queryKey
  alt callback found
    FSM->>Sub: invoke close()
    FSM->>FSM: unregister callback
  else not found
    FSM->>Sentry: captureMessage(...) / log warning
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibble code where snapshots play,
I stitch factories to chase the fray.
When queries vanish, I gently close,
Tests hop in rows, each green carrot shows.
A tidy burrow — build, run, and grow. 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/react-query

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (104)
packages/zustand-firestore/src/store/__tests__/useContextStoreSubscription.test.ts (5)

16-21: Wrap emission in act and assert via waitFor to avoid effect timing flakiness

The subscription is typically established in a React effect. Wrapping the emission in act and asserting with waitFor reduces flakiness across React/test environments.

Apply these diffs:

-import { renderHook } from '@testing-library/react'
+import { renderHook, act, waitFor } from '@testing-library/react'
-  it('subscribes to context store and updates state', () => {
+  it('subscribes to context store and updates state', async () => {
-    subject.next({ value: 2 })
-    expect(store.getState()).toEqual({ value: 2 })
+    act(() => {
+      subject.next({ value: 2 })
+    })
+    await waitFor(() => {
+      expect(store.getState()).toEqual({ value: 2 })
+    })

1-7: Resolve ESLint import resolution errors for react/testing-library/rxjs/zustand

ESLint flags import-x/no-unresolved for react, @testing-library/react, rxjs, and zustand. In a workspace, ensure this package declares appropriate peerDeps/devDeps and that ESLint’s resolver knows about your TS project references.

Recommendations:

  • In packages/zustand-firestore/package.json:
    • peerDependencies: react, rxjs, zustand (versions matching the workspace), with devDependencies mirroring these for tests.
    • devDependencies (tests/build): @testing-library/react, vitest.
  • Configure ESLint resolver (e.g., .eslintrc.js) so import-x resolves workspace projects:
    • settings.import/resolver.typescript.project: ['packages/*/tsconfig.json']
    • settings.import/resolver.node.extensions: ['.ts', '.tsx', '.js', '.jsx']

If helpful, I can propose concrete package.json and ESLint config snippets aligned with your repo’s versions.


1-1: Rename file to kebab-case to satisfy unicorn/filename-case

ESLint suggests renaming useContextStoreSubscription.test.ts to use-context-store-subscription.test.ts. If you switch to a JSX wrapper later, consider .test.tsx.


16-18: Minor: asObservable() is optional here

Subject is already an Observable. Passing subject directly works and can simplify the call.

-      useContextStoreSubscription(Ctx, subject.asObservable())
+      useContextStoreSubscription(Ctx, subject)

5-5: Add a cleanup test to ensure unsubscription on unmount

Verifying unsubscription guards against leaks and accidental updates after teardown.

Apply these diffs:

-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
-  })
-})
+  })
+
+  it('unsubscribes from observable on unmount', () => {
+    const subject = new Subject<{ value: number }>()
+    const store = createStore<{ value: number }>(() => ({ value: 0 }))
+    const Ctx = createContext<StoreApi<{ value: number }>>(store)
+    const spy = vi.spyOn(store, 'setState')
+
+    const { unmount } = renderHook(() => {
+      useContextStoreSubscription(Ctx, subject)
+    })
+    unmount()
+
+    act(() => {
+      subject.next({ value: 123 })
+    })
+    expect(spy).not.toHaveBeenCalled()
+  })
+})

Also applies to: 21-22

packages/zustand-firestore/src/store/__tests__/useStoreSubscription.test.ts (5)

1-5: Import act to avoid React testing warnings during updates

Emitting values into the Subject triggers state updates; importing and using act prevents “not wrapped in act(...)” warnings and ensures effects flush before assertions.

Apply this diff:

-import { renderHook } from '@testing-library/react'
+import { renderHook, act } from '@testing-library/react'

17-19: Wrap Subject emissions in act to flush effects before asserting

This avoids potential timing flakiness and React act warnings.

Apply this diff:

-    subject.next({ value: 1 })
-    expect(store.getState()).toEqual({ value: 1 })
+    act(() => {
+      subject.next({ value: 1 })
+    })
+    expect(store.getState()).toEqual({ value: 1 })

20-21: Optional: Wrap unmount() in act for consistent cleanup semantics

Not strictly required with RTL’s renderHook, but wrapping can silence edge-case act warnings in stricter setups.

Apply this diff:

-    unmount()
+    act(() => {
+      unmount()
+    })

1-1: Rename file to kebab-case per ESLint rule

ESLint (unicorn/filename-case) expects kebab-case. Suggested rename:

  • useStoreSubscription.test.ts → use-store-subscription.test.ts

1-4: Dev dependencies are declared correctly; verify ESLint import resolver setup

  • packages/zustand-firestore/package.json already lists:
    • rxjs (v7.8.2)
    • zustand (v5.0.7)
    • @testing-library/react (v16.3.0)
  • No additional devDependencies are required.
  • If you’re still seeing import-x/no-unresolved:
    • Confirm your ESLint config (via @valian/eslint-config) includes import/resolver settings for Node, TypeScript, and workspace paths.
    • Install/configure eslint-import-resolver-node and eslint-import-resolver-typescript so imports from @testing-library/react, rxjs, and zustand resolve correctly.
packages/react-kitchen-sink/src/react-query/observers/__tests__/documentSnapshotQueryClientObserver.test.ts (5)

16-18: Tighten assertion on next() to verify exact payload and call order

Assert with arguments and call order so regressions are caught precisely.

Apply this diff:

-    observer.next({ isLoading: false, hasError: false, disabled: false } as DocumentSnapshotState)
-    expect(client.setQueryData).toHaveBeenCalled()
+    const state = { isLoading: false, hasError: false, disabled: false } as DocumentSnapshotState
+    observer.next(state)
+    expect(client.setQueryData).toHaveBeenNthCalledWith(1, queryKey, state)

1-4: Assert Sentry capture on error and make call verification deterministic

Since the observer reports errors via captureException, assert it was called and also verify the 2nd call args to setQueryData.

Apply this diff:

+import { captureException } from '@sentry/react'
-    observer.error(new Error('x'))
-    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
+    const err = new Error('x')
+    observer.error(err)
+    expect(captureException).toHaveBeenCalledWith(err)
+    expect(client.setQueryData).toHaveBeenNthCalledWith(2, queryKey, {
       isLoading: false,
       hasError: true,
       disabled: false,
     })

Also applies to: 19-25


26-32: Also verify the complete() call order and total invocations

This makes the test resilient to extra/duplicated updates.

Apply this diff:

-    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
+    expect(client.setQueryData).toHaveBeenNthCalledWith(3, queryKey, {
       isLoading: false,
       hasError: false,
       disabled: true,
     })
+    expect(client.setQueryData).toHaveBeenCalledTimes(3)

1-1: Type queryKey as QueryKey (readonly) to align with React Query typing

Improves type safety and avoids widening to mutable string[].

Apply this diff:

-import { type QueryClient } from '@tanstack/react-query'
+import { type QueryClient, type QueryKey } from '@tanstack/react-query'
-    const queryKey = ['k']
+    const queryKey: QueryKey = ['k'] as const

Also applies to: 13-13


1-1: Rename test file to kebab-case to satisfy unicorn/filename-case

ESLint suggests: document-snapshot-query-client-observer.test.ts.

packages/zustand-firestore/src/contexts/__tests__/createQuerySnapshotStoreContext.test.ts (1)

1-1: Test filename casing vs. rule unicorn/filename-case.

Either rename the file to kebab-case (create-query-snapshot-store-context.test.ts) or disable the rule for test files in ESLint config. If you prefer a one-off suppression, add a file-level disable.

packages/zustand-firestore/src/store/__tests__/createDocumentSnapshotStore.test.ts (1)

1-1: Filename casing nitpick.

If enforcing unicorn/filename-case, rename to kebab-case (create-document-snapshot-store.test.ts) or relax the rule for test files.

packages/zustand-firestore/src/contexts/__tests__/createDocumentSnapshotStoreContext.test.ts (1)

1-1: Filename casing nitpick.

Align with unicorn/filename-case by renaming to kebab-case (create-document-snapshot-store-context.test.ts) or disable the rule for tests.

packages/zustand-firestore/src/store/__tests__/createQuerySnapshotStore.test.ts (1)

8-15: Make the test less brittle by asserting a partial shape.

If new state fields are added later, this exact equality will fail unnecessarily. Using a partial match focuses on invariants that matter here.

-    expect(store.getState()).toEqual({
+    expect(store.getState()).toMatchObject({
       empty: true,
       size: 0,
       isLoading: false,
       hasError: false,
       disabled: true,
       data: [],
     })
packages/react-kitchen-sink/src/react-query/observers/__tests__/querySnapshotQueryClientObserver.test.ts (4)

16-25: Make the “next” assertion precise and isolate phases.

Asserting only “called” can mask regressions. Recommend asserting the exact payload and clearing mocks before the next phase to keep expectations independent.

Apply this diff:

-    observer.next({
-      empty: false,
-      size: 1,
-      isLoading: false,
-      hasError: false,
-      disabled: false,
-      data: [{}],
-    } as QuerySnapshotState)
-    expect(client.setQueryData).toHaveBeenCalled()
+    const snapshot = {
+      empty: false,
+      size: 1,
+      isLoading: false,
+      hasError: false,
+      disabled: false,
+      data: [{}],
+    } as QuerySnapshotState
+    observer.next(snapshot)
+    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, snapshot)
+    expect(client.setQueryData).toHaveBeenCalledTimes(1)
+    vi.clearAllMocks()

1-9: Assert Sentry capture on error (import the mocked module).

You already mock Sentry; import it to assert capture is invoked with the thrown error.

Apply this diff to import the mocked module:

 import { mock } from 'vitest-mock-extended'
 
+import * as Sentry from '@sentry/react'
 import { querySnapshotQueryClientObserver } from '../querySnapshotQueryClientObserver'

26-37: Verify Sentry capture, and isolate error/complete assertions.

This tightens guarantees that errors are reported and keeps each phase’s expectations independent.

Apply this diff:

-    observer.error(new Error('x'))
+    const error = new Error('x')
+    observer.error(error)
+    expect(Sentry.captureException).toHaveBeenCalledWith(error)
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       empty: true,
       size: 0,
       isLoading: false,
       hasError: true,
       disabled: false,
       data: [],
     })
+    expect(client.setQueryData).toHaveBeenCalledTimes(1)
+    vi.clearAllMocks()
 
-    observer.complete()
+    observer.complete()
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       empty: true,
       size: 0,
       isLoading: false,
       hasError: false,
       disabled: true,
       data: [],
     })
+    expect(Sentry.captureException).not.toHaveBeenCalled()
+    expect(client.setQueryData).toHaveBeenCalledTimes(1)

1-1: Filename violates kebab-case rule flagged by ESLint.

Either rename to kebab-case or add an override for tests if you prefer the current style.

Options:

  • Rename file to: query-snapshot-query-client-observer.test.ts
  • Or add an ESLint override for test files in .eslintrc:
    {
    "overrides": [
    {
    "files": ["/*.test.ts", "/*.test.tsx"],
    "rules": { "unicorn/filename-case": "off" }
    }
    ]
    }
packages/react-kitchen-sink/rollup.config.js (1)

4-18: Consider adding react/jsx-runtime to external for React 17+ automatic JSX runtime

If the TS config uses jsx: "react-jsx" (common), Rollup may try to bundle react/jsx-runtime unless it’s explicitly externalized. Safe, low-effort tweak:

   'observable-hooks',
   'react',
+  'react/jsx-runtime',
   'rxjs',
   'type-fest',
packages/zustand-firestore/src/hooks/__tests__/useSnapshotListenOptions.test.ts (4)

1-30: Rename file to kebab-case to satisfy lint rule

ESLint (unicorn/filename-case) requires kebab-case. Please rename:

Current:

  • packages/zustand-firestore/src/hooks/tests/useSnapshotListenOptions.test.ts

Suggested:

  • packages/zustand-firestore/src/hooks/tests/use-snapshot-listen-options.test.ts

13-14: Nit: rerender without extraneous props

The hook under test doesn’t accept props in this case; passing {} to rerender is unnecessary.

-    rerender({})
+    rerender()

17-28: Type consistency: 'source' isn’t part of Firestore’s SnapshotListenOptions

Firestore’s public SnapshotListenOptions only defines includeMetadataChanges; source belongs to get/read options. Since the hook under test intentionally supports source too, make the test’s props explicit with an extended type to avoid excess property errors and clarify intent.

-import { type SnapshotListenOptions } from '@firebase/firestore'
+import type { SnapshotListenOptions } from 'firebase/firestore'
+type ExtendedSnapshotListenOptions = SnapshotListenOptions & {
+  source?: 'default' | 'cache' | 'server'
+}

-  it('keeps only defined fields', () => {
-    const { result, rerender } = renderHook((props) => useSnapshotListenOptions(props), {
-      initialProps: { includeMetadataChanges: true } as SnapshotListenOptions,
-    })
+  it('keeps only defined fields', () => {
+    const { result, rerender } = renderHook<ExtendedSnapshotListenOptions>(
+      (props) => useSnapshotListenOptions(props),
+      { initialProps: { includeMetadataChanges: true } },
+    )

Follow-up: confirm that the implementation’s accepted type matches this extended shape, or refactor the hook’s input type accordingly if this is the intended API.


17-28: Add coverage for boolean false and referential stability with unchanged props

False values are “defined” and should be retained. Also worth asserting identity stability when props remain the same across rerenders.

You can append this test:

it('retains false booleans and memoizes with unchanged props', () => {
  const { result, rerender } = renderHook((props?: { includeMetadataChanges?: boolean }) =>
    useSnapshotListenOptions(props ?? {}),
  )
  rerender({ includeMetadataChanges: false })
  expect(result.current).toEqual({ includeMetadataChanges: false })
  const sameRef = result.current
  rerender({ includeMetadataChanges: false })
  expect(result.current).toBe(sameRef)
})
packages/react-kitchen-sink/src/schema/firestoreDataConverter.ts (1)

22-24: Annotate explicit return type (AppModelType) to lock in the contract.

Small ergonomics win and clearer intent for consumers; avoids accidental widening if inference changes.

-export const firestoreDataConverter = <AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
-  snapshot: QueryDocumentSnapshot<AppModelType, DbModelType>,
-) => handleFirestoreDataTypes(snapshot.data())
+export const firestoreDataConverter = <
+  AppModelType = DocumentData,
+  DbModelType extends DocumentData = DocumentData
+>(
+  snapshot: QueryDocumentSnapshot<AppModelType, DbModelType>,
+): AppModelType => handleFirestoreDataTypes(snapshot.data())
packages/react-kitchen-sink/src/schema/__tests__/firestoreDataConverter.test.ts (1)

25-27: Type the mock with the two generic parameters to mirror production signature.

Aligns the test type with the updated function signature and avoids potential inference drift.

-    const snapshot = mock<QueryDocumentSnapshot>()
+    const snapshot = mock<QueryDocumentSnapshot<DocumentData, DocumentData>>()
packages/react-kitchen-sink/src/schema/__tests__/ZodSchemaError.test.ts (1)

1-1: Rename file to kebab-case to satisfy filename-case rule

Unicorn filename-case rule expects test files to be kebab-case.

Rename:

  • ZodSchemaError.test.ts → zod-schema-error.test.ts
vitest.config.mjs (1)

1-7: Prefer defineWorkspace for multi-project setup

Using defineWorkspace simplifies multi-package discovery and mirrors Vitest’s recommended pattern for monorepos.

Apply:

-import { defineConfig } from 'vitest/config'
-
-export default defineConfig({
-  test: {
-    projects: ['packages/*'],
-  },
-})
+import { defineWorkspace } from 'vitest/config'
+
+export default defineWorkspace([
+  'packages/*',
+])
packages/react-kitchen-sink/src/sentry/index.ts (1)

4-5: Consider importing via the sentry barrel to reduce path churn

Now that these listeners are exported here, prefer: import { sentryDocumentSnapshotListener } from '../sentry' in call sites instead of deep file paths.

packages/rxjs-firebase/src/subjects/QuerySnapshotSubject.ts (1)

32-35: Complete the BehaviorSubject on close() to signal downstream teardown

You stop the source via takeUntil, but the subject itself remains open. Completing the subject helps subscribers finalize and prevents dangling observers.

Apply this diff:

   close(): void {
     this.notification$.next()
     this.notification$.complete()
+    this.complete()
   }
packages/react-kitchen-sink/package.json (2)

39-50: Add @firebase/firestore to devDependencies for test-time resolution

Tests and ESLint import resolution in this package reference @firebase/firestore. Keeping it as a peer is correct for consumers, but adding it to devDependencies ensures local tests/builds resolve and compile.

Apply this diff:

   "devDependencies": {
     "vitest-mock-extended": "^3.1.0",
     "@sentry/core": "^10.1.0",
     "@sentry/react": "^10.1.0",
     "@tanstack/react-query": "^5.84.2",
+    "@firebase/firestore": "^4.7.0",
     "firebase": "^12.0.0",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "rxjs": "^7.8.2",
     "zod": "^4.0.17",
     "zod-firebase": "^2.0.0",
     "zustand": "^5"
   },

62-64: Avoid duplicating a dependency in both peer and optional deps; use peerDependenciesMeta instead

Publishing the same package in both peerDependencies and optionalDependencies can lead to confusing installs and duplicate copies. Prefer marking the peer as optional via peerDependenciesMeta.

Apply this diff to replace optionalDependencies with peerDependenciesMeta:

-  "optionalDependencies": {
-    "zustand": "^5"
-  },
+  "peerDependenciesMeta": {
+    "zustand": {
+      "optional": true
+    }
+  },
packages/react-kitchen-sink/src/sentry/__tests__/sentryDocumentSnapshotListener.test.ts (3)

1-1: Rename file to kebab-case to satisfy lint rule

ESLint’s unicorn/filename-case requires kebab-case. Suggest: sentry-document-snapshot-listener.test.ts.


22-37: Consider tightening assertions for Sentry payloads

Optionally assert the breadcrumb messages and captureException metadata (tags/fingerprint) to guard against regressions in the Sentry integration contract.

Example:

expect(addBreadcrumb).toHaveBeenCalledWith(
  expect.objectContaining({
    category: 'firestore',
    message: 'received document snapshot for (/c/id)',
  }),
)

expect(captureException).toHaveBeenCalledWith(error, expect.objectContaining({
  tags: expect.objectContaining({ firestore: true, 'schema.collection': 'c' }),
  fingerprint: ['firestore', 'c'],
}))

1-2: Ensure @firebase/firestore is installed and ESLint passes in this package

  1. Add @firebase/firestore to packages/react-kitchen-sink/package.json devDependencies:
    "devDependencies": {
      // …
  • "@firebase/firestore": "^4.7.0"
    }
2. Re-run ESLint on the Sentry listener tests to confirm no more unresolved-import errors:
```bash
pnpm -w --filter @valian/react-kitchen-sink exec eslint \
  src/sentry/__tests__/sentryDocumentSnapshotListener.test.ts \
  src/sentry/__tests__/sentrySchemaQuerySnapshotListener.test.ts
  1. If ESLint still reports import/no-unresolved:
    – In your root ESLint config (eslint.config.mjs), enable the TypeScript resolver:
    export default {
      settings: {
        'import/resolver': {
          typescript: true,
        },
      },
    };
    – Or switch to eslint-plugin-import-x with its pnpm-workspace/TS resolver.
packages/zustand-firestore/vitest.config.mjs (1)

3-25: Config looks good; consider restoring mocks between tests

The jsdom env, coverage, and strict thresholds are clear and deliberate. Adding restoreMocks: true complements clearMocks and helps prevent accidental state leakage from jest/vitest mocks.

Apply this diff:

   test: {
     environment: 'jsdom',
-    globals: false,
-    clearMocks: true,
+    globals: false,
+    clearMocks: true,
+    restoreMocks: true,
packages/zustand-firestore/README.md (6)

43-53: Memoize Firestore query to avoid unnecessary resubscriptions and store churn

Creating the query object inside the component body will re-create it on every render. Unless your hook normalizes identity (e.g., via queryEqual), this can cause resubscriptions or a new store instance each render.

Consider memoizing the query:

 export function TodoList() {
-  const todosQuery = query(
-    collection(db, 'todos'),
-    where('completed', '==', false),
-  )
+  const todosQuery = useMemo(
+    () =>
+      query(
+        collection(db, 'todos'),
+        where('completed', '==', false),
+      ),
+    [],
+  )

60-66: Avoid index as React list key; prefer a stable identifier

Using the array index as a key can lead to reconciliation issues on reorders/insertions. Prefer a unique, stable key (e.g., Firestore document id).

-      {data.map((todo, index) => (
-        <li key={index}>{todo.title}</li>
-      ))}
+      {data.map((todo) => (
+        <li key={todo.id}>{todo.title}</li>
+      ))}

90-97: Memoize document reference to avoid resubscriptions

Similar to queries, constructing a DocumentReference on each render can cause resubscriptions if the hook compares by reference.

 export function UserProfile({ userId }: { userId: string }) {
-  const ref = doc(db, 'users', userId)
+  const ref = useMemo(() => doc(db, 'users', userId), [userId])

117-121: Optional: memoize conditional ref for consistency

Not required, but aligning with the memoization pattern can help prevent accidental resubscriptions when parent props change.

 export function MaybeUserProfile({ userId }: { userId?: string }) {
-  const ref = userId ? doc(db, 'users', userId) : null
+  const ref = useMemo(
+    () => (userId ? doc(db, 'users', userId) : null),
+    [userId],
+  )

132-136: Clarify “?” markers in API shape to avoid ambiguity

The “?” markers can be misread as TypeScript optional property syntax. Explicitly state what “optional” means in runtime terms (e.g., undefined before first snapshot or when disabled).

 Both stores expose the same state shapes as `@valian/rxjs-firebase`:
 
 - Query store state includes: `data[]`, `size`, `empty`, `isLoading`, `hasError`, `disabled`, and `snapshot`.
-- Document store state includes: `data?`, `exists?`, `isLoading`, `hasError`, `disabled`, and `snapshot`.
+- Document store state includes: `data?`, `exists?`, `isLoading`, `hasError`, `disabled`, and `snapshot`.
+
+Note:
+- A `?` indicates the field may be `undefined` before the first snapshot arrives or when the subscription is disabled.
+- `exists` reflects the last known document existence; it is `undefined` until the first snapshot.

141-147: Tighten wording in “Contributing/Support” for clarity and tone

Minor stylistic improvement.

-## Contributing
-
-Contributions are welcome! Please feel free to submit a Pull Request.
+## Contributing
+
+Contributions are welcome—open a Pull Request anytime.
 
 ## Support
 
-If you encounter any issues or have questions, please open an issue in the repository.
+If you have questions or run into issues, please open an issue in this repository.
packages/react-kitchen-sink/eslint.config.mjs (2)

32-35: Use vitest plugin rules via .rules instead of spreading the entire config block

Spreading ...vitest.configs.recommended into a per-files object can merge unintended keys. For Flat Config, it’s safer to add the plugin and spread only .rules.

-  {
-    files: ['**/*.{test,spec}.{ts,tsx}'],
-    ...vitest.configs.recommended,
-  },
+  {
+    files: ['**/*.{test,spec}.{ts,tsx}'],
+    plugins: { vitest },
+    // If you rely on global test APIs, also add:
+    // languageOptions: { globals: vitest.environments.env.globals },
+    rules: {
+      ...vitest.configs.recommended.rules,
+    },
+  },

36-41: Consolidate test overrides and consider relaxing filename-case for tests

You can merge this block into the one above to avoid duplication. If you intend to keep camelCase test filenames (as in current tests), consider turning off unicorn/filename-case for test files to silence the warnings flagged by static analysis.

-  {
-    files: ['**/*.{test,spec}.{ts,tsx}'],
-    rules: {
-      'max-classes-per-file': 'off',
-    },
-  },
+  {
+    files: ['**/*.{test,spec}.{ts,tsx}'],
+    plugins: { vitest },
+    // languageOptions: { globals: vitest.environments.env.globals },
+    rules: {
+      ...vitest.configs.recommended.rules,
+      'max-classes-per-file': 'off',
+      // Optional: if you want to keep camelCase tests
+      // 'unicorn/filename-case': 'off',
+    },
+  },

Would you like me to update the config to disable filename-case for tests, or do you prefer to rename the files to kebab-case?

packages/react-kitchen-sink/vitest.config.mjs (1)

3-25: LGTM; consider adding lcov for local tooling compatibility

Config looks solid. If you use tools like Codecov or prefer local HTML viewing, adding 'lcov' (and/or 'html') as coverage reporters can help.

-      reporter: ['text', 'cobertura'],
+      reporter: ['text', 'cobertura', 'lcov'],
packages/react-kitchen-sink/src/sentry/__tests__/sentrySchemaQuerySnapshotListener.test.ts (1)

17-43: Strengthen expectations to prove both breadcrumbs are emitted and error context is captured

Current assertions only check that breadcrumbs/errors were emitted at least once, which could miss regressions. Assert call counts or payloads to ensure all three hooks emit the intended telemetry.

   it('forwards calls and adds breadcrumbs/captures errors', () => {
@@
-    listener.onSnapshot?.({
+    listener.onSnapshot?.({
       isLoading: false,
       hasError: false,
       disabled: false,
       empty: true,
       size: 0,
       data: [],
       snapshot: stub<QuerySnapshot>(),
     } as const)
-    expect(onSnapshot).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    expect(onSnapshot).toHaveBeenCalledTimes(1)
+    expect(addBreadcrumb).toHaveBeenCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'received query snapshot for (q)',
+      }),
+    )
@@
     const error = new Error('x')
     listener.onError?.(error)
-    expect(onError).toHaveBeenCalledWith(error)
-    expect(captureException).toHaveBeenCalled()
+    expect(onError).toHaveBeenCalledWith(error)
+    expect(captureException).toHaveBeenCalledWith(
+      error,
+      expect.objectContaining({
+        tags: expect.objectContaining({ firestore: true }),
+        fingerprint: ['firestore', 'c'],
+      }),
+    )
@@
     listener.onComplete?.()
-    expect(onComplete).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    expect(onComplete).toHaveBeenCalledTimes(1)
+    expect(addBreadcrumb).toHaveBeenCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'closed query snapshot for (q)',
+      }),
+    )
packages/react-kitchen-sink/src/sentry/__tests__/handleNotAwaitedPromise.test.ts (3)

15-22: Prefer microtask flush over setTimeout for deterministic promise rejection handling

Using a macrotask delay can be flaky. Flushing the microtask queue is sufficient and more deterministic for handling rejections.

   it('captures rejection errors', async () => {
     const error = new Error('boom')
     handleNotAwaitedPromise(Promise.reject(error))
-    await new Promise((resolve) => {
-      setTimeout(resolve, 0)
-    })
+    // Flush microtasks so the rejection handler runs
+    await Promise.resolve()
     expect(captureException).toHaveBeenCalledWith(error, undefined)
   })

30-33: Silence Biome’s noThenProperty for intentional thenable test double

This test intentionally crafts a thenable without catch. Biome flags it; add a targeted ignore to keep linters green without changing behavior.

   it('does nothing when object has no catch', () => {
-    // eslint-disable-next-line unicorn/no-thenable, @typescript-eslint/no-empty-function
+    // eslint-disable-next-line unicorn/no-thenable, @typescript-eslint/no-empty-function
+    /* biome-ignore lint/suspicious/noThenProperty: crafting a thenable-like object for test */
     handleNotAwaitedPromise({ then: () => {} } as unknown as Promise<void>)
     expect(captureException).not.toHaveBeenCalled()
   })

1-8: Optional: add a test to validate context forwarding

You can strengthen coverage by asserting that a provided Sentry capture context is forwarded.

 describe('handleNotAwaitedPromise', () => {
@@
   it('captures rejection errors', async () => {
@@
   })
 
+  it('forwards capture context when provided', async () => {
+    const error = new Error('boom')
+    const context = { tags: { x: 'y' } }
+    handleNotAwaitedPromise(Promise.reject(error), context)
+    await Promise.resolve()
+    expect(captureException).toHaveBeenCalledWith(error, context)
+  })
+
packages/zustand-firestore/src/hooks/__tests__/useDocumentStore.test.ts (3)

2-2: Wrap emissions in act, restore spies, and unmount to avoid test leaks and act warnings

These tests may intermittently warn about “not wrapped in act(...)” and can leak spies/subscriptions across tests. Small fixes:

  • import act
  • restore spies after each test
  • emit inside act
  • unmount the hook

Apply this diff:

-import { renderHook } from '@testing-library/react'
+import { renderHook, act } from '@testing-library/react'
@@
 describe('useDocumentStore', () => {
@@
+  afterEach(() => {
+    vi.restoreAllMocks()
+  })
@@
-    vi.spyOn(rx, 'fromDocumentRef').mockReturnValue(subject.asObservable())
+    vi.spyOn(rx, 'fromDocumentRef').mockReturnValueOnce(subject.asObservable())
@@
-    const { result } = renderHook(() => useDocumentStore({ ref: stub<DocumentReference>() }))
+    const { result, unmount } = renderHook(() => useDocumentStore({ ref: stub<DocumentReference>() }))
@@
-    subject.next(snapshot)
+    act(() => {
+      subject.next(snapshot)
+    })
@@
     expect(result.current.getState()).toEqual({
@@
     })
+    unmount()

Also applies to: 14-15, 22-22, 24-24, 29-29, 38-39


1-7: Linter “unresolved import” hints: ensure resolver/peers are satisfied in tests

ESLint warnings about unresolved modules are likely due to pnpm workspace/peer setup:

  • @firebase/firestore is a peer (good for runtime) but not a dev dep; tests import it directly. Either:
    • add @firebase/firestore to this package’s devDependencies, or
    • switch type-only imports to firebase/firestore, or
    • configure ESLint’s import resolver to consider workspace packages.

Let me know your preference; I can patch the manifest or resolver config.


1-1: Filename doesn’t match kebab-case rule

Rule: unicorn/filename-case. Either rename to use-document-store.test.ts or relax the rule for test files.

packages/zustand-firestore/src/hooks/__tests__/useQueryStore.test.ts (3)

27-52: Sturdy live subscription test; tighten with act/cleanup

The subject-driven snapshot and state mapping looks correct. Minor improvements:

  • emit inside act
  • restore spy after test
  • unmount to avoid lingering subscriptions
  • nit: setting exists() on QueryDocumentSnapshot is unnecessary (always true)

Apply this diff:

-import { renderHook } from '@testing-library/react'
+import { renderHook, act } from '@testing-library/react'
@@
 describe('useQueryStore', () => {
+  afterEach(() => {
+    vi.restoreAllMocks()
+  })
@@
-    vi.spyOn(rx, 'fromQuery').mockReturnValue(subject.asObservable())
+    vi.spyOn(rx, 'fromQuery').mockReturnValueOnce(subject.asObservable())
@@
-    const { result } = renderHook(() => useQueryStore({ query: stub<Query>() }))
+    const { result, unmount } = renderHook(() => useQueryStore({ query: stub<Query>() }))
@@
-    queryDocumentSnapshot.exists.mockReturnValue(true)
+    // exists() is always true for QueryDocumentSnapshot; this line can be removed.
@@
-    subject.next(querySnapshot)
+    act(() => {
+      subject.next(querySnapshot)
+    })
@@
     expect(result.current.getState()).toEqual({
@@
     })
+    unmount()

1-7: Linter “unresolved import” hints mirror the other test file

Same remedies apply: add @firebase/firestore as a devDependency, import types from firebase/firestore, or configure ESLint resolver for the workspace.


1-1: Filename doesn’t match kebab-case rule

Rename to use-query-store.test.ts or relax the rule in ESLint for test files.

packages/react-kitchen-sink/README.md (1)

83-115: Example looks great; optional: prefer stable keys over index

Using the array index as a React key is fine for docs, but a stable identifier (e.g., document id) is preferable in real apps.

packages/zustand-firestore/package.json (1)

31-39: Move runtime deps to peers (good) and add a small DX tweak for tests

  • Moving rxjs and zustand to peerDependencies is appropriate for a library.
  • To resolve ESLint/Vitest complaints about type-only imports from @firebase/firestore in tests, consider adding it as a devDependency as well (it can remain a peer for consumers).

Suggested patch:

   "devDependencies": {
     "firebase": "^12.0.0",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "rxjs": "^7.8.2",
     "zustand": "^5.0.7",
     "@testing-library/react": "^16.3.0",
-    "vitest-mock-extended": "^3.1.0"
+    "vitest-mock-extended": "^3.1.0",
+    "@firebase/firestore": "^4.7.0"
   },

Also applies to: 40-46, 69-76

packages/react-kitchen-sink/src/rxjs/__tests__/documentSnapshotSubject.test.ts (1)

1-1: Rename test file to kebab-case to satisfy ESLint.

Rule unicorn/filename-case suggests document-snapshot-subject.test.ts.

packages/react-kitchen-sink/src/rxjs/__tests__/schemaDocumentSnapshotSubject.test.ts (1)

1-1: Rename test file to kebab-case to satisfy ESLint.

Rule unicorn/filename-case suggests schema-document-snapshot-subject.test.ts.

packages/react-kitchen-sink/src/sentry/__tests__/processErrorWithSentryCaptureContext.test.ts (2)

1-1: Import ErrorEvent from @sentry/core to align with implementation and avoid resolver issues.

The implementation uses the core Sentry event type; importing from @sentry/core keeps tests consistent and may resolve import-x/no-unresolved warnings.

- import { type ErrorEvent } from '@sentry/react'
+ import { type ErrorEvent } from '@sentry/core'

1-1: Rename test file to kebab-case to satisfy ESLint.

Rule unicorn/filename-case suggests process-error-with-sentry-capture-context.test.ts.

packages/react-kitchen-sink/src/rxjs/__tests__/schemaQuerySnapshotSubject.test.ts (2)

8-14: Module mock is fine, but add cleanup to prevent cross-test interference

Add afterEach to clear and restore mocks so subsequent tests aren’t affected by call counts or module mocks.

 vi.mock('@valian/rxjs-firebase', () => ({
   fromQuery: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
   QuerySnapshotSubject: class {
     close = vi.fn()
     subscribe = vi.fn()
   },
 }))
+
+afterEach(() => {
+  vi.clearAllMocks()
+  vi.restoreAllMocks()
+})

26-31: Tighten assertions and ensure type-safe spy return

  1. The spy return value should satisfy the expected Firestore Query type to avoid TS friction.
  2. Assert fromQuery was called with the prepared query object, not just “called”.
-    vi.spyOn(collections.users, 'prepare').mockReturnValue({ id: 'q' })
-    const subject = schemaQuerySnapshotSubject(collections.users, { name: 'q' })
-    expect(collections.users.prepare).toHaveBeenCalled()
-    expect(fromQuery).toHaveBeenCalled()
+    const prepared = { id: 'q' } as unknown as import('@firebase/firestore').Query
+    vi.spyOn(collections.users, 'prepare').mockReturnValue(prepared)
+    const subject = schemaQuerySnapshotSubject(collections.users, { name: 'q' })
+    expect(collections.users.prepare).toHaveBeenCalledWith({ name: 'q' }, undefined)
+    expect(fromQuery).toHaveBeenCalledWith(prepared, undefined)
     expect(subject).toBeInstanceOf(QuerySnapshotSubject)

Would you like me to add a second test that passes options and a listener to verify they’re forwarded correctly?

packages/react-kitchen-sink/src/react-query/__tests__/querySnapshotQueryOptions.test.ts (2)

18-21: Strengthen assertions: verify full meta and queryFn wiring

Add checks for meta fields and that queryFn is a function.

     expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
     expect(snapshotManager.querySnapshotSubjectFactory).toHaveBeenCalledWith(query, snapshotOptions, listener)
     expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.snapshotManager).toBe(snapshotManager)
+    expect(opts.meta?.query).toBe(query)
+    expect(typeof opts.queryFn).toBe('function')

1-6: Address ESLint: filename case and import resolution

  • Rename file to kebab case: query-snapshot-query-options.test.ts.
  • If using TS path aliases, ensure eslint resolver supports TS (e.g., eslint-import-resolver-typescript for eslint-plugin-import-x) to avoid false “no-unresolved”.
packages/react-kitchen-sink/src/react-query/queryFn/__tests__/queryFnFromDocumentSnapshotSubjectFactory.test.ts (5)

3-3: Unify Firestore import path with the rest of the suite

Other tests import types from @firebase/firestore. Align this import to reduce resolver inconsistencies.

-import { type DocumentSnapshot } from 'firebase/firestore'
+import { type DocumentSnapshot } from '@firebase/firestore'

8-12: Add test cleanup for isolation

Clear mocks between tests so setQueryData call counts and spies don’t leak across cases.

 import { queryFnFromDocumentSnapshotSubjectFactory } from '../queryFnFromDocumentSnapshotSubjectFactory'
 
 class MockDocumentSnapshotSubject<T> extends Subject<DocumentSnapshotState<T>> {
   close = vi.fn()
 }
+
+afterEach(() => {
+  vi.clearAllMocks()
+  vi.restoreAllMocks()
+})

15-23: Pass queryKey and assert factory invocation to reduce brittle undefined usage

Make the subjectFactory call explicit by providing queryKey and asserting it was used.

   it('returns loading state when waitForData is false', async () => {
-    const subject = new MockDocumentSnapshotSubject()
-    const queryFn = queryFnFromDocumentSnapshotSubjectFactory(vi.fn().mockReturnValue(subject))
+    const subject = new MockDocumentSnapshotSubject()
+    const factory = vi.fn().mockReturnValue(subject)
+    const queryFn = queryFnFromDocumentSnapshotSubjectFactory(factory)
     const controller = new AbortController()
     const queryClient = mock<QueryClient>()
-    const result = await queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const queryKey = ['k']
+    const result = await queryFn(
+      mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey }),
+    )
     expect(result).toEqual({ isLoading: true, hasError: false, disabled: false })
     expect(subject.close).not.toHaveBeenCalled()
+    expect(factory).toHaveBeenCalledWith(queryKey)
   })

25-34: Abort test: include queryKey and assert factory call for consistency

Make this test consistent with the one above.

   it('closes subject on abort', async () => {
-    const subject = new MockDocumentSnapshotSubject()
-    const queryFn = queryFnFromDocumentSnapshotSubjectFactory(vi.fn().mockReturnValue(subject))
+    const subject = new MockDocumentSnapshotSubject()
+    const factory = vi.fn().mockReturnValue(subject)
+    const queryFn = queryFnFromDocumentSnapshotSubjectFactory(factory)
     const controller = new AbortController()
     const queryClient = mock<QueryClient>()
-    const promise = queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const queryKey = ['k']
+    const promise = queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey }))
     controller.abort()
     await promise
     expect(subject.close).toHaveBeenCalled()
+    expect(factory).toHaveBeenCalledWith(queryKey)
   })

1-7: Address ESLint: filename case and import resolution

  • Rename file to kebab case: query-fn-from-document-snapshot-subject-factory.test.ts.
  • Ensure resolver config supports TS and monorepo packages to eliminate “no-unresolved”.
packages/react-kitchen-sink/src/react-query/__tests__/documentSnapshotQueryOptions.test.ts (2)

18-21: Enhance assertions on meta and queryFn

Verify meta fields and function shape to better lock the contract.

     expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
     expect(snapshotManager.documentSnapshotSubjectFactory).toHaveBeenCalledWith(ref, snapshotOptions, listener)
     expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.snapshotManager).toBe(snapshotManager)
+    expect(opts.meta?.documentRef).toBe(ref)
+    expect(typeof opts.queryFn).toBe('function')

1-6: Address ESLint: filename case and import resolution

  • Rename file to kebab case: document-snapshot-query-options.test.ts.
  • Confirm deps and eslint TS resolver setup to avoid false unresolved import errors.
packages/react-kitchen-sink/src/react-query/__tests__/schemaQuerySnapshotQueryOptions.test.ts (2)

33-41: Assert full meta content for schema-based options

Also check that snapshotManager, collection name, and schemaQuery are reflected in meta.

     expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
     expect(snapshotManager.schemaQuerySnapshotSubjectFactory).toHaveBeenCalledWith(
       collections.users,
       { name: 'q' },
       snapshotOptions,
       listener,
     )
     expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.snapshotManager).toBe(snapshotManager)
+    expect(opts.meta?.collection).toBe(collections.users.collectionName)
+    expect(opts.meta?.schemaQuery).toEqual({ name: 'q' })
+    expect(typeof opts.queryFn).toBe('function')

1-8: Address ESLint: filename case and import resolution

  • Rename file to kebab case: schema-query-snapshot-query-options.test.ts.
  • Ensure zod, zod-firebase and @valian/rxjs-firebase are declared in package.json and that your ESLint resolver supports TS.
packages/react-kitchen-sink/src/rxjs/documentSnapshotSubject.ts (1)

10-14: Optional: add explicit return type for clarity and consistency

Type inference works, but adding the explicit return type improves readability and keeps this factory in sync with the explicit generics used on construction.

Apply this minimal diff:

-) =>
+): DocumentSnapshotSubject<AppModelType, DbModelType> =>
   new DocumentSnapshotSubject<AppModelType, DbModelType>(
     fromDocumentRef(ref, options),
     sentryDocumentSnapshotListener(ref, listener),
   )
packages/react-kitchen-sink/src/sentry/__tests__/ErrorWithSentryCaptureContext.test.ts (2)

1-1: Fix lint: filename case and Sentry type import resolution

  • unicorn/filename-case: rename the file to kebab-case.
  • import-x/no-unresolved: prefer importing ErrorEvent from @sentry/types to avoid depending on @sentry/core in tests.

Suggested changes:

  • Rename file: ErrorWithSentryCaptureContext.test.ts → error-with-sentry-capture-context.test.ts
  • Update the import:
-import { type ErrorEvent } from '@sentry/core'
+import { type ErrorEvent } from '@sentry/types'

If you prefer to keep @sentry/core, ensure the package is listed in this package’s devDependencies to satisfy import linting.


3-3: Resolve import for vitest-mock-extended

ESLint flags import-x/no-unresolved. If the workspace relies on hoisted deps, add vitest-mock-extended to this package’s devDependencies or configure your lint rules for monorepo resolution.

packages/react-kitchen-sink/src/hooks/useSchemaQueryStore.ts (2)

27-27: Consider adding metaOptions and factory to useObservable deps

Currently, the observable won’t re-evaluate when metaOptions or factory change, which could yield stale queries. Add them to the dependency array on Line 62 to reflect all inputs used to compute the stream.

Recommended update (Line 62):

  • Before: [options.query, snapshotListenOptions]
  • After: [options.query, snapshotListenOptions, metaOptions, factory]

15-16: Resolve lint: module resolution for external deps

Static analysis flags unresolved imports for zod-firebase and zustand. If your monorepo rules require local package deps, add these to this package’s dependencies/devDependencies or adjust eslint-plugin-import settings for workspaces.

packages/react-kitchen-sink/src/react-query/queryFn/__tests__/queryFnFromQuerySnapshotSubjectFactory.test.ts (2)

43-101: Optional: use fake timers to reduce flakiness in “waitForData” test

The test relies on setTimeout(…, 0). Using vi.useFakeTimers() and advancing timers can make this fully deterministic and remove timing sensitivities on slower CI runners.

Example pattern (no diff, for illustration):

  • vi.useFakeTimers()
  • schedule subject.next(...)
  • vi.runAllTimers() (or vi.advanceTimersByTime(timeout))
  • await promise

1-1: Fix lint: filename case and unresolved imports

  • unicorn/filename-case: rename file to kebab-case.
  • import-x/no-unresolved: ensure the imported packages are available to this package (workspace or local devDependencies): @firebase/firestore, @tanstack/react-query, @valian/rxjs-firebase, rxjs, vitest-mock-extended.

Rename suggestion:

  • queryFnFromQuerySnapshotSubjectFactory.test.ts → query-fn-from-query-snapshot-subject-factory.test.ts
packages/react-kitchen-sink/src/rxjs/schemaQuerySnapshotSubject.ts (2)

19-22: Return type and listener typing are solid; consider explicit generics for symmetry

The function’s return type is the new SchemaQuerySnapshotSubject alias, which is correct. For symmetry with documentSnapshotSubject, you could pass explicit generics to QuerySnapshotSubject, though inference here is already accurate.

No change required; optional style tweak only.


8-8: Resolve lint: zod-firebase import

ESLint flagged import-x/no-unresolved. Ensure zod-firebase is declared in this package’s dependencies or adjust the monorepo import resolver settings.

packages/react-kitchen-sink/src/react-query/__tests__/FirestoreSnaphotManager.test.ts (2)

10-21: Replace class-based mocks with constructor-function stubs to satisfy max-classes-per-file and keep tests lean

Using two inline classes triggers the ESLint max-classes-per-file rule. Constructor-function stubs provide the same behavior without introducing classes.

Apply this diff:

-vi.mock('@valian/rxjs-firebase', () => ({
-  fromDocumentRef: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
-  fromQuery: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
-  DocumentSnapshotSubject: class {
-    close = vi.fn()
-    subscribe = vi.fn()
-  },
-  QuerySnapshotSubject: class {
-    close = vi.fn()
-    subscribe = vi.fn()
-  },
-}))
+vi.mock('@valian/rxjs-firebase', () => {
+  const DocumentSnapshotSubject = vi.fn(function () {
+    // @ts-expect-error - dynamic test doubles
+    this.close = vi.fn()
+    // @ts-expect-error - dynamic test doubles
+    this.subscribe = vi.fn()
+  })
+  const QuerySnapshotSubject = vi.fn(function () {
+    // @ts-expect-error - dynamic test doubles
+    this.close = vi.fn()
+    // @ts-expect-error - dynamic test doubles
+    this.subscribe = vi.fn()
+  })
+  return {
+    fromDocumentRef: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
+    fromQuery: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
+    DocumentSnapshotSubject,
+    QuerySnapshotSubject,
+  }
+})

1-1: Consider kebab-case filename and fixing the “Snaphot” typo

ESLint suggests kebab-case for filenames. Also, “Snaphot” appears to be a typo of “Snapshot” across files. Consider renaming to keep consistency and discoverability (e.g., firestore-snapshot-manager.test.ts and corresponding source).

packages/react-kitchen-sink/src/rxjs/schemaDocumentSnapshotSubject.ts (2)

8-10: Default TOptions to MetaOutputOptions for ergonomics

Adding a default type makes the API easier to consume when callers don’t need to specialize options.

 export const schemaDocumentSnapshotSubject = <
   TCollectionSchema extends CollectionSchema,
-  TOptions extends MetaOutputOptions,
+  TOptions extends MetaOutputOptions = MetaOutputOptions,
 >(

1-1: Consider kebab-case filename to satisfy ESLint

Rule unicorn/filename-case suggests schema-document-snapshot-subject.ts.

packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromQuerySnapshotSubjectFactory.ts (2)

26-33: Optional: prefer timeout() operator over takeUntil(timer()) for explicit timeout errors

timeout() produces a TimeoutError instead of completing without emission, which can be easier to handle and reason about in React Query.

Apply this diff and adjust imports:

-import { firstValueFrom, skipWhile, takeUntil, timer } from 'rxjs'
+import { firstValueFrom, skipWhile, timeout } from 'rxjs'
@@
-      return firstValueFrom(
-        subject$.pipe(
-          takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
-          skipWhile(({ isLoading }) => isLoading),
-        ),
-      )
+      return firstValueFrom(
+        subject$.pipe(
+          timeout(options.waitForDataTimeout ?? 10_000),
+          skipWhile(({ isLoading }) => isLoading),
+        ),
+      )

1-1: Consider kebab-case filename to satisfy ESLint

Suggested: query-fn-from-query-snapshot-subject-factory.ts.

packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromDocumentSnapshotSubjectFactory.ts (2)

26-33: Optional: prefer timeout() operator over takeUntil(timer())

Ensures a clear TimeoutError is surfaced to React Query instead of an EmptyError due to completion without emission.

Apply this diff and adjust imports:

-import { firstValueFrom, skipWhile, takeUntil, timer } from 'rxjs'
+import { firstValueFrom, skipWhile, timeout } from 'rxjs'
@@
-      return firstValueFrom(
-        subject$.pipe(
-          takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
-          skipWhile(({ isLoading }) => isLoading),
-        ),
-      )
+      return firstValueFrom(
+        subject$.pipe(
+          timeout(options.waitForDataTimeout ?? 10_000),
+          skipWhile(({ isLoading }) => isLoading),
+        ),
+      )

1-1: Consider kebab-case filename to satisfy ESLint

Suggested: query-fn-from-document-snapshot-subject-factory.ts.

packages/react-kitchen-sink/src/react-query/FirestoreSnaphotManager.ts (3)

45-48: Don’t throw on missing subscription; log and continue

Throwing here will crash the app during cache GC if metadata is off or cleanup was already invoked. A warn + bail-out is safer in production.

Apply this diff:

-        const closeSnapshot = this.#onClose.get(snapshotId)
-        if (!closeSnapshot) {
-          throw new Error(`Subscription for key ${snapshotId} not found`)
-        }
+        const closeSnapshot = this.#onClose.get(snapshotId)
+        if (!closeSnapshot) {
+          // Non-fatal: log and skip to avoid crashing during cache cleanup
+          console.warn(`[FirestoreSnapshotManager] Subscription for key ${snapshotId} not found`)
+          return
+        }

36-36: Typo in public class name: “Snaphot” → “Snapshot”

Public API types should avoid typos. Consider renaming the class (and file) to FirestoreSnapshotManager before this ships broadly.

If you opt to rename, update references across the repo (imports and types). I can generate a codemod for the monorepo if helpful.


39-54: Consider exposing a dispose() to unsubscribe the QueryCache listener

If the manager has a defined lifecycle (e.g., per app), keeping the subscription forever may be undesirable. Store the unsubscribe handle and provide dispose().

Example addition outside the selected range:

// store unsubscribe in a private field and expose dispose()
private #unsubscribe?: () => void

constructor(queryClient: QueryClient) {
  this.#unsubscribe = queryClient.getQueryCache().subscribe((event: QueryCacheNotifyEvent) => {
    // ...
  })
}

dispose() {
  this.#unsubscribe?.()
  this.#unsubscribe = undefined
}
packages/react-kitchen-sink/src/react-query/querySnapshotQueryOptions.ts (3)

52-67: Pass only waitForData options to the factory; avoid leaking unrelated props

Passing the entire props object to queryFnFromQuerySnapshotSubjectFactory is unnecessary; only waitForData and waitForDataTimeout are consumed.

Apply this diff:

-  }: QuerySnapshotQueryOptions<AppModelType, DbModelType, TError, TData, TQueryKey>,
+  }: QuerySnapshotQueryOptions<AppModelType, DbModelType, TError, TData, TQueryKey>,
 ): QuerySnapshotQueryOptionsResult<AppModelType, DbModelType, TError, TData, TQueryKey> =>
   queryOptions({
     queryFn: queryFnFromQuerySnapshotSubjectFactory(
-      snapshotManager.querySnapshotSubjectFactory(query, snapshotOptions, listener),
-      props,
+      snapshotManager.querySnapshotSubjectFactory(query, snapshotOptions, listener),
+      { waitForData: props.waitForData, waitForDataTimeout: props.waitForDataTimeout },
     ),

Also applies to: 68-71


72-77: Drop redundant initialData: undefined

initialData is already omitted from the public options type. Setting it to undefined is redundant.

Apply this diff:

   staleTime: Infinity,
   retry: false,
   gcTime: 10_000,
-  initialData: undefined,
   ...props,

15-15: Follow-up if you rename the manager class

If you fix the “Snaphot” typo in the class/file, remember to update this import:

Apply this diff after the rename:

-import { type FirestoreSnaphotManager } from './FirestoreSnaphotManager'
+import { type FirestoreSnapshotManager } from './FirestoreSnapshotManager'
packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (2)

52-66: Pass only waitForData options to the factory

Limit the second argument to queryFnFromDocumentSnapshotSubjectFactory to its actual option surface.

Apply this diff:

-  }: DocumentSnapshotQueryOptions<AppModelType, DbModelType, TError, TData, TQueryKey>,
+  }: DocumentSnapshotQueryOptions<AppModelType, DbModelType, TError, TData, TQueryKey>,
 ): DocumentSnapshotQueryOptionsResult<AppModelType, DbModelType, TError, TData, TQueryKey> =>
   queryOptions({
     queryFn: queryFnFromDocumentSnapshotSubjectFactory(
-      snapshotManager.documentSnapshotSubjectFactory(ref, snapshotOptions, listener),
-      props,
+      snapshotManager.documentSnapshotSubjectFactory(ref, snapshotOptions, listener),
+      { waitForData: props.waitForData, waitForDataTimeout: props.waitForDataTimeout },
     ),

Also applies to: 67-71


15-15: Follow-up if you rename the manager class

Update the import when fixing the manager class/file typo.

Apply this diff after the rename:

-import { type FirestoreSnaphotManager } from './FirestoreSnaphotManager'
+import { type FirestoreSnapshotManager } from './FirestoreSnapshotManager'
packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (1)

60-75: Pass only waitForData options to the factory

Avoid passing the entire props as factory options; only waitForData fields are used.

Apply this diff:

-  }: SchemaQuerySnapshotQueryOptions<TCollectionSchema, TOptions, TError, TData, TQueryKey>,
+  }: SchemaQuerySnapshotQueryOptions<TCollectionSchema, TOptions, TError, TData, TQueryKey>,
 ): SchemaQuerySnapshotQueryOptionsResult<TCollectionSchema, TOptions, TError, TData, TQueryKey> =>
   queryOptions({
     queryFn: queryFnFromQuerySnapshotSubjectFactory(
-      snapshotManager.schemaQuerySnapshotSubjectFactory(factory, query, snapshotOptions, listener),
-      props,
+      snapshotManager.schemaQuerySnapshotSubjectFactory(factory, query, snapshotOptions, listener),
+      { waitForData: props.waitForData, waitForDataTimeout: props.waitForDataTimeout },
     ),

Also applies to: 76-81

Comment thread packages/react-kitchen-sink/README.md Outdated
@Nr9 Nr9 force-pushed the feat/react-query branch from cf493e3 to 2cd7276 Compare August 16, 2025 00:03
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (9)
packages/react-kitchen-sink/src/schema/__tests__/ZodSchemaError.test.ts (3)

1-1: Import Firestore type from the public module

Use the public Firestore entrypoint to avoid internal package coupling and resolver errors.

-import { type QueryDocumentSnapshot } from '@firebase/firestore'
+import { type QueryDocumentSnapshot } from 'firebase/firestore'

2-2: Use the correct Sentry Event type; fix unresolved import and typing

@sentry/react doesn’t export an ErrorEvent suited for beforeSend; use Event from @sentry/types.

-import { type ErrorEvent } from '@sentry/react'
+import { type Event as SentryEvent } from '@sentry/types'
-    const event = error.beforeSend(mock<ErrorEvent>())
+    const event = error.beforeSend(mock<SentryEvent>())

Also applies to: 19-19


1-1: Rename test file to kebab-case to satisfy lint rule

ESLint (unicorn/filename-case) requires kebab-case for filenames. Rename:

git mv packages/react-kitchen-sink/src/schema/__tests__/ZodSchemaError.test.ts \
       packages/react-kitchen-sink/src/schema/__tests__/zod-schema-error.test.ts
packages/react-kitchen-sink/src/rxjs/__tests__/schemaQuerySnapshotSubject.test.ts (1)

1-4: Fix ESLint findings: rename file to kebab-case and configure import resolver

  • Rename file to: packages/react-kitchen-sink/src/rxjs/tests/schema-query-snapshot-subject.test.ts.
  • Configure ESLint’s import resolver for your monorepo to resolve @valian/rxjs-firebase, zod, and zod-firebase. With eslint-plugin-import-x, use the typescript and node resolvers.

Example .eslintrc settings (adjust paths to your tsconfigs):

settings: {
  'import-x/resolver': {
    typescript: {
      alwaysTryTypes: true,
      project: ['tsconfig.base.json', 'packages/*/tsconfig.json'],
    },
    node: {
      extensions: ['.ts', '.tsx', '.js', '.jsx'],
    },
  },
}

Run this script to verify the rename and resolver settings are present:

#!/bin/bash
set -euo pipefail

echo "Check renamed test file"
if [ -f packages/react-kitchen-sink/src/rxjs/__tests__/schema-query-snapshot-subject.test.ts ]; then
  echo "OK: kebab-case test file exists"
else
  echo "MISSING: packages/react-kitchen-sink/src/rxjs/__tests__/schema-query-snapshot-subject.test.ts"
fi

echo
echo "Scan ESLint config(s) for import(-x) resolver setup"
fd -HI '(\.eslintrc.*|eslint\.config\.(js|cjs|ts|mjs))' -t f \
  --exec bash -lc 'echo "---- {} ----"; rg -n -C2 "(import(-x)?/resolver|typescript.*resolver)" "{}" || true'
packages/react-kitchen-sink/src/react-query/observers/__tests__/querySnapshotQueryClientObserver.test.ts (1)

1-4: Fix ESLint “unresolved import” warnings by ensuring deps/resolver are set (duplicate of prior review).

The unresolved import errors for @tanstack/react-query, @valian/rxjs-firebase, and vitest-mock-extended match the earlier review. Please ensure:

  • @valian/rxjs-firebase is added to devDependencies for this package.
  • ESLint import resolver is configured for workspaces/TS.

See the previous comment and script already provided; same guidance applies here.

packages/react-kitchen-sink/src/sentry/__tests__/sentrySchemaQuerySnapshotListener.test.ts (1)

1-1: Import QuerySnapshot type from the public SDK

This mirrors prior feedback: prefer 'firebase/firestore' over '@firebase/firestore' for stability and to satisfy no-unresolved.

Apply this diff:

-import { type QuerySnapshot } from '@firebase/firestore'
+import type { QuerySnapshot } from 'firebase/firestore'
packages/react-kitchen-sink/src/schema/__tests__/firestoreDataConverter.test.ts (1)

1-2: Do not import from '@firebase/firestore'; use the public 'firebase/firestore' entry.

Importing from the internal package can break types/bundling. Consolidate on the public entry.

-import { type QueryDocumentSnapshot } from '@firebase/firestore'
-import { GeoPoint, Timestamp } from 'firebase/firestore'
+import { type QueryDocumentSnapshot, GeoPoint, Timestamp } from 'firebase/firestore'
packages/react-kitchen-sink/README.md (1)

51-51: Remove unused QueryClient import from example.

It’s not used in the snippet.

-import { QueryClient, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
+import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
packages/react-kitchen-sink/src/rxjs/__tests__/schemaDocumentSnapshotSubject.test.ts (1)

1-1: Fix spy return type: doc() returns DocumentReference, and assert fromDocumentRef args

Factory.read.doc returns a DocumentReference, not a DocumentSnapshot. Update the spy and strengthen the assertion to validate the exact args passed to fromDocumentRef.

Apply:

- import { type DocumentSnapshot } from '@firebase/firestore'
+ import { type DocumentReference } from '@firebase/firestore'
@@
-    const documentSnapshot = mock<DocumentSnapshot>()
-    vi.spyOn(collections.users.read, 'doc').mockReturnValue(documentSnapshot)
+    const documentRef = mock<DocumentReference>()
+    vi.spyOn(collections.users.read, 'doc').mockReturnValue(documentRef)
@@
-    expect(fromDocumentRef).toHaveBeenCalled()
+    expect(fromDocumentRef).toHaveBeenCalledWith(documentRef, undefined)

Also applies to: 28-35, 33-33

🧹 Nitpick comments (43)
packages/react-kitchen-sink/src/rxjs/__tests__/schemaQuerySnapshotSubject.test.ts (3)

8-14: Make mocks verifiable: use a mock constructor and track calls

Your current class-based mock works for instanceof, but you can’t assert constructor calls/args. Switching to a mock constructor preserves instanceof and enables call assertions.

Apply this diff:

-vi.mock('@valian/rxjs-firebase', () => ({
-  fromQuery: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
-  QuerySnapshotSubject: class {
-    close = vi.fn()
-    subscribe = vi.fn()
-  },
-}))
+vi.mock('@valian/rxjs-firebase', () => {
+  const fromQuery = vi.fn().mockReturnValue({ subscribe: vi.fn() })
+  const QuerySnapshotSubject = vi.fn(function (this: any) {
+    this.close = vi.fn()
+    this.subscribe = vi.fn()
+  })
+  return { fromQuery, QuerySnapshotSubject }
+})

25-31: Strengthen assertions: verify options pass-through and constructor invocation

Assert the arguments passed to prepare and fromQuery, and that QuerySnapshotSubject is constructed. Also pass options to validate the pass-through.

   it('creates a QuerySnapshotSubject from factory.prepare', () => {
-    vi.spyOn(collections.users, 'prepare').mockReturnValue({ id: 'q' })
-    const subject = schemaQuerySnapshotSubject(collections.users, { name: 'q' })
-    expect(collections.users.prepare).toHaveBeenCalled()
-    expect(fromQuery).toHaveBeenCalled()
-    expect(subject).toBeInstanceOf(QuerySnapshotSubject)
+    vi.spyOn(collections.users, 'prepare').mockReturnValue({ id: 'q' } as any)
+    const options = { includeMetadataChanges: true } as const
+    const subject = schemaQuerySnapshotSubject(collections.users, { name: 'q' }, options)
+    expect(collections.users.prepare).toHaveBeenCalledWith({ name: 'q' }, options)
+    expect(fromQuery).toHaveBeenCalledWith({ id: 'q' }, options)
+    expect(QuerySnapshotSubject).toHaveBeenCalledTimes(1)
+    expect(subject).toBeInstanceOf(QuerySnapshotSubject)
   })

2-2: Test hygiene: restore mocks between tests

Restore spies/mocks to avoid cross-test leakage.

-import { describe, expect, it, vi } from 'vitest'
+import { describe, expect, it, vi, afterEach } from 'vitest'
@@
 
+afterEach(() => {
+  vi.restoreAllMocks()
+})

Also applies to: 15-15

packages/react-kitchen-sink/src/react-query/observers/__tests__/querySnapshotQueryClientObserver.test.ts (6)

1-1: Rename file to kebab-case to satisfy ESLint unicorn/filename-case.

Rename to: query-snapshot-query-client-observer.test.ts


8-8: Optional: Place vi.mock before importing the SUT to avoid hoist surprises.

Vitest hoists vi.mock, so this works. For clarity and to avoid confusion if hoist behavior changes, consider moving the mock above the import of querySnapshotQueryClientObserver.


12-15: Mocking QueryClient is fine; consider mockDeep for robustness.

Current approach works. If you want stronger type guarantees for class methods and future calls, use mockDeep from vitest-mock-extended.

Apply this diff:

-import { mock } from 'vitest-mock-extended'
+import { mockDeep } from 'vitest-mock-extended'
...
-    const client = mock<QueryClient>({ setQueryData: vi.fn() })
+    const client = mockDeep<QueryClient>()

16-25: Strengthen the “next” assertion by verifying the exact payload.

Asserting only “was called” can miss shape regressions. Capture the object and assert with the same reference.

Apply this diff:

-    observer.next({
-      empty: false,
-      size: 1,
-      isLoading: false,
-      hasError: false,
-      disabled: false,
-      data: [{}],
-    } as QuerySnapshotState)
-    expect(client.setQueryData).toHaveBeenCalled()
+    const snapshotState: QuerySnapshotState = {
+      empty: false,
+      size: 1,
+      isLoading: false,
+      hasError: false,
+      disabled: false,
+      data: [{}],
+    }
+    observer.next(snapshotState)
+    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, snapshotState)

26-35: Also assert Sentry captureException is called on error.

This test already verifies the cache update on error. Also check that Sentry receives the error.

Apply this diff:

-    observer.error(new Error('x'))
+    const err = new Error('x')
+    observer.error(err)
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       empty: true,
       size: 0,
       isLoading: false,
       hasError: true,
       disabled: false,
       data: [],
     })
+    expect(captureException).toHaveBeenCalledWith(err)

Additionally add this import at the top of the file:

import { captureException } from '@sentry/react'

36-45: Optional: Assert the total number of cache writes to guard call count.

This makes the test resilient to accidental extra updates.

Apply this diff:

     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       empty: true,
       size: 0,
       isLoading: false,
       hasError: false,
       disabled: true,
       data: [],
     })
+    expect(client.setQueryData).toHaveBeenCalledTimes(3)
packages/react-kitchen-sink/src/sentry/__tests__/processErrorWithSentryCaptureContext.test.ts (1)

18-20: Strengthen assertion: verify parent beforeSend runs before cause

Given the recursion order, root error.beforeSend should run before cause.beforeSend. This makes the behavior explicit and guards against regressions.

Apply this diff:

     expect(spy).toHaveBeenCalledWith(event)
     expect(causeSpy).toHaveBeenCalledWith(event)
+    // Ensure the root error's beforeSend runs before the cause's beforeSend
+    expect(spy.mock.invocationCallOrder[0]).toBeLessThan(causeSpy.mock.invocationCallOrder[0])
packages/react-kitchen-sink/src/sentry/__tests__/sentryDocumentSnapshotListener.test.ts (1)

22-41: Assert breadcrumb and captureException payloads for stronger guarantees

Verify the breadcrumb messages and error context/fingerprints/tags to ensure Sentry integration is wired as designed.

Apply this diff:

-    listener.onSnapshot?.({
-      isLoading: false,
-      hasError: false,
-      disabled: false,
-      snapshot: stub<DocumentSnapshot>(),
-      exists: true,
-      data: {},
-    } as const)
-    expect(onSnapshot).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    const state = {
+      isLoading: false,
+      hasError: false,
+      disabled: false,
+      snapshot: stub<DocumentSnapshot>(),
+      exists: true,
+      data: {},
+    } as const
+    listener.onSnapshot?.(state)
+    expect(onSnapshot).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'received document snapshot for (/c/id)',
+        data: expect.objectContaining({ state }),
+      }),
+    )
@@
-    listener.onError?.(error)
-    expect(onError).toHaveBeenCalledWith(error)
-    expect(captureException).toHaveBeenCalled()
+    listener.onError?.(error)
+    expect(onError).toHaveBeenCalledWith(error)
+    expect(captureException).toHaveBeenCalledWith(
+      error,
+      expect.objectContaining({
+        tags: expect.objectContaining({ firestore: true, 'schema.collection': 'c' }),
+        fingerprint: ['firestore', 'c'],
+        contexts: expect.objectContaining({
+          snapshot: expect.objectContaining({ ref }),
+        }),
+      }),
+    )
@@
-    listener.onComplete?.()
-    expect(onComplete).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    listener.onComplete?.()
+    expect(onComplete).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'closed document snapshot for (/c/id)',
+      }),
+    )
packages/react-kitchen-sink/src/sentry/__tests__/sentrySchemaQuerySnapshotListener.test.ts (1)

23-43: Assert breadcrumb messages and captureException context

Strengthen the test by validating the exact breadcrumb messages and the Sentry context, tags, and fingerprint.

Apply this diff:

-    listener.onSnapshot?.({
-      isLoading: false,
-      hasError: false,
-      disabled: false,
-      empty: true,
-      size: 0,
-      data: [],
-      snapshot: stub<QuerySnapshot>(),
-    } as const)
-    expect(onSnapshot).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    const state = {
+      isLoading: false,
+      hasError: false,
+      disabled: false,
+      empty: true,
+      size: 0,
+      data: [],
+      snapshot: stub<QuerySnapshot>(),
+    } as const
+    listener.onSnapshot?.(state)
+    expect(onSnapshot).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'received query snapshot for (q)',
+        data: expect.objectContaining({ state }),
+      }),
+    )
@@
-    listener.onError?.(error)
-    expect(onError).toHaveBeenCalledWith(error)
-    expect(captureException).toHaveBeenCalled()
+    listener.onError?.(error)
+    expect(onError).toHaveBeenCalledWith(error)
+    expect(captureException).toHaveBeenCalledWith(
+      error,
+      expect.objectContaining({
+        tags: expect.objectContaining({ firestore: true, 'schema.collection': 'c' }),
+        fingerprint: ['firestore', 'c'],
+        contexts: expect.objectContaining({
+          query: expect.objectContaining({ query }),
+        }),
+      }),
+    )
@@
-    listener.onComplete?.()
-    expect(onComplete).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    listener.onComplete?.()
+    expect(onComplete).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'closed query snapshot for (q)',
+      }),
+    )
packages/react-kitchen-sink/src/sentry/__tests__/handleNotAwaitedPromise.test.ts (2)

32-33: Suppress Biome false positive for crafted thenable

Biome flags adding a 'then' property, but here it's intentional to validate the guard. Add a biome-ignore to keep lint clean.

Apply this diff:

   it('does nothing when object has no catch', () => {
-    // eslint-disable-next-line unicorn/no-thenable, @typescript-eslint/no-empty-function
+    // eslint-disable-next-line unicorn/no-thenable, @typescript-eslint/no-empty-function
+    /* biome-ignore lint/suspicious/noThenProperty: intentionally constructing non-promise thenable to test guard */
     handleNotAwaitedPromise({ then: () => {} } as unknown as Promise<void>)
     expect(captureException).not.toHaveBeenCalled()
   })

15-22: Optional: use microtask flush instead of setTimeout for stability

Replacing the zero-timeout with a microtask flush makes the test slightly faster and less flaky.

Apply this diff:

-    await new Promise((resolve) => {
-      setTimeout(resolve, 0)
-    })
+    // Flush microtasks so the rejection handler runs
+    await Promise.resolve()
packages/react-kitchen-sink/src/react-query/observers/__tests__/documentSnapshotQueryClientObserver.test.ts (4)

12-18: Assert the exact payload on next() rather than just "called".

Verifying the precise state written to the cache makes this test more robust.

Apply this diff:

-    observer.next({ isLoading: false, hasError: false, disabled: false } as DocumentSnapshotState)
-    expect(client.setQueryData).toHaveBeenCalled()
+    const state = { isLoading: false, hasError: false, disabled: false } as DocumentSnapshotState
+    observer.next(state)
+    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, state)

8-8: Import captureException to assert it’s invoked on error.

You already mock @sentry/react. Import captureException so the test can assert it was called with the thrown error.

+import { captureException } from '@sentry/react'
 vi.mock('@sentry/react', () => ({ captureException: vi.fn() }))

19-25: Also assert Sentry is called with the error.

This closes the loop on the observer’s error path.

-    observer.error(new Error('x'))
+    const err = new Error('x')
+    observer.error(err)
+    expect(captureException).toHaveBeenCalledWith(err)
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       isLoading: false,
       hasError: true,
       disabled: false,
     })

1-1: Rename file to kebab-case to satisfy ESLint.

ESLint unicorn/filename-case suggests: document-snapshot-query-client-observer.test.ts

packages/react-kitchen-sink/src/schema/firestoreDataConverter.ts (2)

15-16: Use a “plain object” guard to avoid transforming class instances.

value instanceof Object matches many class instances. Restrict recursion to plain objects to avoid unintended transformations (e.g., DocumentReference, custom classes).

-    case value instanceof Object:
+    case typeof value === 'object' && value !== null && value.constructor === Object:
       return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, handleFirestoreDataTypes(v)])) as T

1-1: Rename file to kebab-case to satisfy ESLint.

ESLint unicorn/filename-case suggests: firestore-data-converter.ts

packages/react-kitchen-sink/src/schema/__tests__/firestoreDataConverter.test.ts (2)

27-29: Optionally assert snapshot.data() was called once.

This tightens the contract of the converter relying on snapshot.data().

     const out = firestoreDataConverter(snapshot)
 
+    expect(snapshot.data).toHaveBeenCalledTimes(1)
     expect(out.date).toBe(now)

1-1: Rename file to kebab-case to satisfy ESLint.

ESLint unicorn/filename-case suggests: firestore-data-converter.test.ts

packages/react-kitchen-sink/README.md (1)

56-59: Memoize FirestoreSnapshotManager to avoid re-instantiating on each render.

Creating a new manager per render is unnecessary; memoize it against the client.

+import { useMemo } from 'react'
@@
   const client = useQueryClient()
-  const manager = new FirestoreSnapshotManager(client)
+  const manager = useMemo(() => new FirestoreSnapshotManager(client), [client])
packages/react-kitchen-sink/src/rxjs/__tests__/documentSnapshotSubject.test.ts (1)

1-1: Rename file to kebab-case to satisfy ESLint.

ESLint unicorn/filename-case suggests: document-snapshot-subject.test.ts

packages/react-kitchen-sink/src/rxjs/__tests__/schemaDocumentSnapshotSubject.test.ts (1)

1-6: Address ESLint errors: filename case and unresolved imports

  • Rename file to schema-document-snapshot-subject.test.ts (unicorn/filename-case).
  • Ensure ESLint import resolver knows the monorepo/TS project references, or add missing deps in the relevant package.

Consider adding (or verifying) this in your ESLint config if you're using eslint-plugin-import-x:

  • settings:
    • import-x/resolver:
      • typescript:
        • project: true

Alternatively, disable import resolution for test files if it’s a false positive in your workspace setup.

packages/react-kitchen-sink/src/react-query/__tests__/querySnapshotQueryOptions.test.ts (2)

18-21: Tighten assertions: validate meta fields and default options

Verify we carry through metadata and the expected defaults (staleTime/gcTime/retry).

Apply:

     expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
     expect(snapshotManager.querySnapshotSubjectFactory).toHaveBeenCalledWith(query, snapshotOptions, listener)
     expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.snapshotManager).toBe(snapshotManager)
+    expect(opts.meta?.query).toBe(query)
+    expect(opts.retry).toBe(false)
+    expect(opts.staleTime).toBe(Infinity)
+    expect(opts.gcTime).toBe(10_000)

1-6: Fix ESLint issues: filename case and import resolution

  • Rename file to query-snapshot-query-options.test.ts.
  • Configure import resolver for the monorepo/TS project or ensure packages are resolvable in this workspace.

If needed, add an import-x typescript resolver configuration similar to the one suggested in the other test, or relax the rule for test files if these are false positives.

packages/react-kitchen-sink/src/react-query/queryFn/__tests__/queryFnFromDocumentSnapshotSubjectFactory.test.ts (3)

27-35: Pass a queryKey in the loading-state test to avoid undefined key usage

queryFn subscribes with a queryClient observer bound to the queryKey; passing one keeps the test semantically correct and future-proof.

Apply:

-    const result = await queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const result = await queryFn(
+      mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey: ['k'] }),
+    )

48-76: Make timing deterministic with fake timers

These tests rely on setTimeout(0). Using vi.useFakeTimers() + vi.runAllTimers() reduces flakiness and speeds up the suite.

Example refactor within this test:

   it('waits for non-loading snapshot when waitForData is true', async () => {
@@
-    const controller = new AbortController()
+    const controller = new AbortController()
+    vi.useFakeTimers()
@@
-    setTimeout(() => {
+    setTimeout(() => {
       subject.next({
         isLoading: false,
         hasError: false,
         disabled: false,
         exists: true,
         data: {},
         snapshot,
       })
     }, 0)
-    const result = await promise
+    vi.runAllTimers()
+    const result = await promise

You can apply the same pattern in the "default timeout" test below.


1-6: ESLint issues: filename case and unresolved imports

  • Rename to query-fn-from-document-snapshot-subject-factory.test.ts.
  • Ensure rxjs and other imports resolve in this package or configure the import resolver.

Adopt the same ESLint resolver settings used elsewhere in tests if these are workspace-related false positives.

packages/react-kitchen-sink/src/react-query/__tests__/documentSnapshotQueryOptions.test.ts (2)

18-21: Expand assertions to cover meta fields and defaults

Check that meta carries the ref and snapshotManager, and defaults are applied.

Apply:

     expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
     expect(snapshotManager.documentSnapshotSubjectFactory).toHaveBeenCalledWith(ref, snapshotOptions, listener)
     expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.snapshotManager).toBe(snapshotManager)
+    expect(opts.meta?.documentRef).toBe(ref)
+    expect(opts.retry).toBe(false)
+    expect(opts.staleTime).toBe(Infinity)
+    expect(opts.gcTime).toBe(10_000)

1-6: Resolve ESLint issues: filename and import resolution

  • Rename to document-snapshot-query-options.test.ts.
  • Ensure the ESLint import resolver and dependencies are configured for this workspace.

Use a shared ESLint config for tests that sets the import-x typescript resolver to project: true or relax import resolution for test files if needed.

packages/react-kitchen-sink/src/react-query/__tests__/schemaQuerySnapshotQueryOptions.test.ts (2)

1-7: Address filename case and resolver for monorepo paths

  • Rename the file to kebab-case to satisfy the rule.
  • The import-x/no-unresolved errors are often due to missing TS path resolver config in ESLint for monorepos. Ensure eslint settings include a TS resolver pointing at your project tsconfig(s).

Example (eslint config):

  • settings.import-x/resolver.typescript.project should include workspace tsconfigs (e.g., ["tsconfig.base.json", "packages/*/tsconfig.json"]).
  • File rename: schema-query-snapshot-query-options.test.ts

20-41: Strengthen assertions: check queryFn presence and meta completeness

Good coverage of the factory call and meta.type. Consider asserting the presence of queryFn and the full meta payload to prevent regressions.

Apply this diff to add assertions:

   it('builds query options with meta and queryFn from manager factory', () => {
@@
     })
 
-    expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
+    expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
+    expect(typeof opts.queryFn).toBe('function')
@@
-    expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta).toMatchObject({
+      snapshotManager,
+      collection: collections.users.collectionName,
+      schemaQuery: { name: 'q' },
+    })
   })
packages/react-kitchen-sink/src/react-query/queryFn/__tests__/queryFnFromQuerySnapshotSubjectFactory.test.ts (3)

1-7: Fix filename case and resolver warnings for tests

  • Rename the file to kebab case to satisfy unicorn/filename-case.
  • Configure ESLint’s TS resolver to avoid import-x/no-unresolved for workspace packages.

Rename to: query-fn-from-query-snapshot-subject-factory.test.ts

ESLint settings suggestion:

  • settings.import-x/resolver.typescript.project: include your workspace tsconfigs.

51-60: Stabilize abort-close assertion to avoid race flakiness

Since the queryFn returns synchronously (waitForData=false), abort and the close callback may be processed on the next tick. Add a small tick/yield before the assertion to prevent flaky failures.

Apply this diff:

   const promise = queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
   controller.abort()
-  await promise
-  expect(subject.close).toHaveBeenCalled()
+  await promise
+  // yield to allow abort handler to run
+  await new Promise((r) => setTimeout(r, 0))
+  expect(subject.close).toHaveBeenCalled()

62-120: Add coverage for waitForData + early abort (cancellation semantics)

Currently, abort during waitForData relies on the timeout to settle. Add a test ensuring the Promise settles on abort without waiting for the timeout (once implementation supports it).

Proposed test to add:

it('settles promptly when aborted while waiting for data', async () => {
  const subject = new MockQuerySnapshotSubject()
  const queryFn = queryFnFromQuerySnapshotSubjectFactory(vi.fn().mockReturnValue(subject), {
    waitForData: true,
    waitForDataTimeout: 10_000,
  })
  const controller = new AbortController()
  const queryClient = mock<QueryClient>()
  const promise = queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey: ['k'] }))
  controller.abort()
  await expect(promise).rejects.toThrow()
})

If you want, I can wire the abort signal into the Rx pipeline in the implementation to make this pass.

packages/react-kitchen-sink/src/react-query/__tests__/FirestoreSnapshotManager.test.ts (1)

1-1: Rename test file to kebab case and consider relaxing max-classes-per-file for tests

  • Rename to firestore-snapshot-manager.test.ts.
  • max-classes-per-file is triggered by inline mock classes. Either:
    • disable the rule for this file, or
    • change mocks to return plain objects (as in the diff above) to avoid class declarations.

Example disable at top of file:
/* eslint-disable max-classes-per-file */

packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromQuerySnapshotSubjectFactory.ts (2)

42-49: Propagate abort signal into waitForData pipeline to cancel promptly

When waitForData is true, aborts currently rely on the timeout to settle. Wire the AbortSignal into the stream so the Promise resolves/rejects promptly on abort, avoiding up to 10s hangs.

Apply this diff to the pipeline:

-      return firstValueFrom(
-        subject$.pipe(
-          takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
-          skipWhile(({ isLoading }) => isLoading),
-        ),
-      )
+      return firstValueFrom(
+        subject$.pipe(
+          // complete on timeout or abort, whichever comes first
+          takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
+          skipWhile(({ isLoading }) => isLoading),
+        ),
+      )

And add abort to the takeUntil with an extra operator outside this range:

  • Import fromEvent and merge from rxjs.
  • Replace the single takeUntil(timer(...)) with:
    takeUntil(merge(timer(options.waitForDataTimeout ?? 10_000), fromEvent(signal, 'abort')))

Outside-range import addition:

// add to imports
import { fromEvent, merge } from 'rxjs'

I can update the tests to cover abort during waitForData once this is in.


1-1: Rename to kebab-case to satisfy filename-case rule

Rename file to query-fn-from-query-snapshot-subject-factory.ts.

packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromDocumentSnapshotSubjectFactory.ts (2)

39-46: Add abort awareness to the waitForData stream

Mirror the query snapshot variant: ensure abort cancels the waitForData Promise promptly rather than waiting for timeout.

Apply this diff to the pipeline:

-      return firstValueFrom(
-        subject$.pipe(
-          takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
-          skipWhile(({ isLoading }) => isLoading),
-        ),
-      )
+      return firstValueFrom(
+        subject$.pipe(
+          // complete on timeout or abort, whichever comes first
+          takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
+          skipWhile(({ isLoading }) => isLoading),
+        ),
+      )

Outside-range import addition and change (same as the query snapshot file):

  • Add fromEvent and merge imports.
  • Replace takeUntil(timer(...)) with takeUntil(merge(timer(options.waitForDataTimeout ?? 10_000), fromEvent(signal, 'abort'))).
import { fromEvent, merge } from 'rxjs'

1-1: Rename to kebab-case to satisfy filename-case rule

Rename file to query-fn-from-document-snapshot-subject-factory.ts.

packages/react-kitchen-sink/src/react-query/FirestoreSnapshotManager.ts (1)

46-48: Consider whether throwing an error is appropriate here.

The error thrown when a subscription is not found could crash the application. Since the query cache removal event is triggered by the framework, consider logging a warning instead or handling this gracefully, as it might occur during edge cases like hot module replacement or concurrent cleanup operations.

Consider this safer approach:

 const closeSnapshot = this.#onClose.get(snapshotId)
 if (!closeSnapshot) {
-  throw new Error(`Subscription for key ${snapshotId} not found`)
+  console.warn(`FirestoreSnapshotManager: Subscription for key ${snapshotId} not found during cleanup`)
+  return
 }

@Nr9 Nr9 force-pushed the feat/react-query branch from 2cd7276 to 42d563d Compare August 16, 2025 00:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🔭 Outside diff range comments (1)
packages/react-kitchen-sink/src/schema/__tests__/ZodSchemaError.test.ts (1)

1-26: Rename file to satisfy filename-case rule

ESLint (unicorn/filename-case) requires kebab-case for test filenames. Rename to: zod-schema-error.test.ts

♻️ Duplicate comments (14)
packages/react-kitchen-sink/README.md (1)

51-51: Remove unused QueryClient import from the example.

The example doesn’t use QueryClient; keep imports minimal.

Apply this diff:

-import { QueryClient, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
+import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
packages/react-kitchen-sink/src/rxjs/__tests__/schemaDocumentSnapshotSubject.test.ts (1)

28-35: Fix spy return type and strengthen assertion: doc() returns DocumentReference, not DocumentSnapshot.

As per the factory and schemaDocumentSnapshotSubject implementation, read.doc returns a DocumentReference, which is then fed into fromDocumentRef (see packages/rxjs-firebase/src/source/fromDocumentRef.ts). The test currently mocks a DocumentSnapshot and only checks that fromDocumentRef was called, not that it received the expected reference and options. Update the spy to return a DocumentReference and assert the exact call.

Apply:

-    const documentSnapshot = mock<DocumentSnapshot>()
-    vi.spyOn(collections.users.read, 'doc').mockReturnValue(documentSnapshot)
+    const documentRef = mock<DocumentReference>()
+    vi.spyOn(collections.users.read, 'doc').mockReturnValue(documentRef)
@@
-    expect(fromDocumentRef).toHaveBeenCalled()
+    expect(fromDocumentRef).toHaveBeenCalledWith(documentRef, undefined)
packages/react-kitchen-sink/src/sentry/__tests__/sentrySchemaQuerySnapshotListener.test.ts (1)

1-1: Use public Firebase import path for QuerySnapshot

Avoid the internal '@firebase/firestore' import to fix no-unresolved and prevent coupling to private internals. Use the public 'firebase/firestore' type import.

-import { type QuerySnapshot } from '@firebase/firestore'
+import type { QuerySnapshot } from 'firebase/firestore'
packages/react-kitchen-sink/src/schema/__tests__/ZodSchemaError.test.ts (3)

12-12: Make the message assertion resilient to path formatting

Assert using the snapshot’s path rather than a hardcoded string. Optionally align the mocked path with Firestore’s format (no leading slash).

Apply this diff:

-      ref: { path: '/c/id', parent: { id: 'c' } },
+      ref: { path: 'c/id', parent: { id: 'c' } },
@@
-    expect(error.message).toBe('Zod Error for /c/id')
+    expect(error.message).toBe(`Zod Error for ${snapshot.ref.path}`)

Also applies to: 17-17


1-1: Import Firestore types from the public module to fix resolver error

Use the public Firestore entrypoint; the internal package isn’t resolvable and isn’t a stable API surface.

Apply this diff:

-import { type QueryDocumentSnapshot } from '@firebase/firestore'
+import { type QueryDocumentSnapshot } from 'firebase/firestore'

2-2: Use the correct Sentry Event type; fix unresolved import and typing

beforeSend receives an Event from @sentry/types, not ErrorEvent from @sentry/react. This also resolves the linter error.

Apply this diff:

-import { type ErrorEvent } from '@sentry/react'
+import { type Event as SentryEvent } from '@sentry/types'
@@
-    const event = error.beforeSend(mock<ErrorEvent>())
+    const event = error.beforeSend(mock<SentryEvent>())

Also applies to: 19-19

packages/react-kitchen-sink/src/sentry/__tests__/sentryDocumentSnapshotListener.test.ts (1)

1-1: Use public Firestore types, not internal '@firebase/firestore'

Importing from '@firebase/firestore' couples tests to internal modules and fails lint/module resolution. Switch to the public SDK path.

-import { type DocumentReference, type DocumentSnapshot } from '@firebase/firestore'
+import type { DocumentReference, DocumentSnapshot } from 'firebase/firestore'
packages/react-kitchen-sink/src/sentry/__tests__/processErrorWithSentryCaptureContext.test.ts (1)

1-1: Fix type import: use public Sentry types package

Import the Sentry event type from '@sentry/types' (public types package) instead of '@sentry/react' to resolve no-unresolved and keep type deps minimal.

Apply this diff:

-import { type ErrorEvent } from '@sentry/react'
+import type { Event as ErrorEvent } from '@sentry/types'
packages/react-kitchen-sink/src/sentry/__tests__/ErrorWithSentryCaptureContext.test.ts (1)

1-1: Fix type import: use public Sentry types package

Import the event type from '@sentry/types' instead of '@sentry/core' to fix no-unresolved and keep only the public types dependency.

Apply this diff:

-import { type ErrorEvent } from '@sentry/core'
+import type { Event as ErrorEvent } from '@sentry/types'
packages/react-kitchen-sink/src/schema/__tests__/firestoreDataConverter.test.ts (1)

1-2: Do not import from '@firebase/firestore'; use the public 'firebase/firestore' entry

Stick to the public module to avoid bundling/type issues and to satisfy lint rules. Also consolidate imports.

-import { type QueryDocumentSnapshot } from '@firebase/firestore'
-import { GeoPoint, Timestamp } from 'firebase/firestore'
+import { type DocumentData, type QueryDocumentSnapshot, GeoPoint, Timestamp } from 'firebase/firestore'

Optionally, be explicit with generics on your mock to align with the production signature:

-    const snapshot = mock<QueryDocumentSnapshot>()
+    const snapshot = mock<QueryDocumentSnapshot<DocumentData, DocumentData>>()
packages/react-kitchen-sink/src/rxjs/__tests__/documentSnapshotSubject.test.ts (1)

1-1: Use the public Firestore entry point (firebase/firestore)

Importing from the internal @firebase/firestore package causes type/bundle issues and ESLint import resolution errors. Switch to the public entry.

Apply this diff:

-import { type DocumentReference } from '@firebase/firestore'
+import { type DocumentReference } from 'firebase/firestore'
packages/react-kitchen-sink/src/rxjs/__tests__/schemaQuerySnapshotSubject.test.ts (1)

1-1: ESLint import resolution for workspace packages

If ESLint flags @valian/rxjs-firebase, configure eslint-plugin-import resolvers for monorepo/TypeScript as noted in another comment.

packages/react-kitchen-sink/src/react-query/observers/__tests__/querySnapshotQueryClientObserver.test.ts (1)

1-5: Unresolved imports and filename-case: align package deps and naming

As noted earlier, ensure @valian/rxjs-firebase (and other test-time imports) are present in devDependencies and ESLint resolvers are configured. Rename file to kebab-case per rule.

  • querySnapshotQueryClientObserver.test.ts -> query-snapshot-query-client-observer.test.ts

You can reuse the dependency/resolver script from the first file’s comment.

packages/react-kitchen-sink/src/react-query/__tests__/FirestoreSnapshotManager.test.ts (1)

11-25: Stabilize tests by mocking local rxjs wrappers used by FirestoreSnapshotManager

FirestoreSnapshotManager constructs subjects via local wrappers (../rxjs and ../rxjs/schemaDocumentSnapshotSubject). While your current mocks of @valian/rxjs-firebase are sufficient for the underlying classes, mocking the local wrappers makes the tests fully isolated and avoids accidental behavior changes if those wrappers add logic.

Apply this diff to add the missing mocks (note the paths are relative to this test file):

 vi.mock('@sentry/react', () => ({
   captureMessage: vi.fn(),
 }))
 vi.mock('@valian/rxjs-firebase', () => ({
   fromDocumentRef: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
   fromQuery: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
   DocumentSnapshotSubject: class {
     close = vi.fn()
     subscribe = vi.fn()
   },
   QuerySnapshotSubject: class {
     close = vi.fn()
     subscribe = vi.fn()
   },
 }))
+vi.mock('../../rxjs', () => ({
+  documentSnapshotSubject: vi.fn().mockReturnValue({ close: vi.fn(), subscribe: vi.fn() }),
+  schemaQuerySnapshotSubject: vi.fn().mockReturnValue({ close: vi.fn(), subscribe: vi.fn() }),
+}))
+vi.mock('../../rxjs/schemaDocumentSnapshotSubject', () => ({
+  schemaDocumentSnapshotSubject: vi.fn().mockReturnValue({ close: vi.fn(), subscribe: vi.fn() }),
+}))
🧹 Nitpick comments (64)
packages/react-kitchen-sink/README.md (5)

50-50: Use tsx code fences for JSX-containing examples.

Both examples include JSX; using tsx improves syntax highlighting.

Apply these diffs:

-```typescript
+```tsx
-```typescript
+```tsx

Also applies to: 82-82


57-58: Memoize the manager to avoid re-instantiation on every render.

Creating FirestoreSnapshotManager on each render is avoidable; memoize it.

Apply this diff:

-  const client = useQueryClient()
-  const manager = new FirestoreSnapshotManager(client)
+  const client = useQueryClient()
+  const manager = useMemo(() => new FirestoreSnapshotManager(client), [client])

Add the missing import at the top of that snippet:

import { useMemo } from 'react'

84-84: Remove unused useSchemaDocumentStore import.

Only useSchemaQueryStore is used in the example.

Apply this diff:

-import { useSchemaDocumentStore, useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'
+import { useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'

109-111: Prefer stable keys over array indices in lists.

Use a document ID or another stable unique field to prevent unnecessary re-renders.

Apply this diff if todo.id is available:

-      {data.map((todo, i) => (
-        <li key={i}>{todo.title}</li>
-      ))}
+      {data.map((todo) => (
+        <li key={todo.id}>{todo.title}</li>
+      ))}

123-123: Tighten phrasing in the contributing section.

Shorter and more direct wording reads better.

Apply this diff:

-Contributions are welcome! Please feel free to submit a Pull Request.
+Contributions welcome—open a pull request.
packages/react-kitchen-sink/src/rxjs/__tests__/schemaDocumentSnapshotSubject.test.ts (2)

10-16: Ensure the mock is effectively applied before module usage.

In Vitest, mocks are hoisted, but ordering can still surprise when dependencies are imported eagerly. To be safe, declare vi.mock('@valian/rxjs-firebase', ...) before importing modules that depend on it (e.g., schemaDocumentSnapshotSubject) or convert the subject import into a dynamic import after the mock.

If you prefer to keep static imports, move this mock above Line 8. Alternatively:

vi.mock('@valian/rxjs-firebase', () => ({ /* ... */ }))
const { schemaDocumentSnapshotSubject } = await import('../schemaDocumentSnapshotSubject')

1-1: Rename file to kebab-case to satisfy lint rule.

ESLint flags filename casing. Rename to schema-document-snapshot-subject.test.ts.

packages/react-kitchen-sink/src/sentry/__tests__/handleNotAwaitedPromise.test.ts (4)

15-22: Prefer microtask flush over setTimeout and assert single invocation

Using setTimeout(0) flushes a macro task and is slower/flakier than flushing the microtask queue. Also, asserting the number of calls prevents silent double-captures.

Apply this diff:

   it('captures rejection errors', async () => {
     const error = new Error('boom')
     handleNotAwaitedPromise(Promise.reject(error))
-    await new Promise((resolve) => {
-      setTimeout(resolve, 0)
-    })
-    expect(captureException).toHaveBeenCalledWith(error, undefined)
+    // Flush microtasks; ensures the attached .catch runs
+    await Promise.resolve()
+    await Promise.resolve()
+    expect(captureException).toHaveBeenCalledWith(error, undefined)
+    expect(captureException).toHaveBeenCalledTimes(1)
   })

30-34: Silence Biome false positive for intentional thenable

This test intentionally creates a thenable without catch. Biome flags it (lint/suspicious/noThenProperty). Ignore the rule locally to keep the test intent explicit.

Apply this diff:

   it('does nothing when object has no catch', () => {
-    // eslint-disable-next-line unicorn/no-thenable, @typescript-eslint/no-empty-function
+    // biome-ignore lint/suspicious/noThenProperty: intentionally creating a thenable without catch to verify guard
+    // eslint-disable-next-line unicorn/no-thenable, @typescript-eslint/no-empty-function
     handleNotAwaitedPromise({ then: () => {} } as unknown as Promise<void>)
     expect(captureException).not.toHaveBeenCalled()
   })

1-36: Rename test file to kebab-case to satisfy lint rule

ESLint (unicorn/filename-case) wants kebab-case. Rename file to: handle-not-awaited-promise.test.ts.

Would you like me to include the rename and update any references in a follow-up commit?


10-35: Optional: add coverage for context and non-function catch

Two edge cases worth locking down:

  • Passing a CaptureContext should forward it to captureException.
  • Objects with a catch property that is not a function should be ignored (current implementation guards against this).

I can add tests like:

it('forwards CaptureContext when provided', async () => {
  const error = new Error('boom')
  const context = { level: 'error', tags: { foo: 'bar' } } as any
  handleNotAwaitedPromise(Promise.reject(error), context)
  await Promise.resolve()
  await Promise.resolve()
  expect(captureException).toHaveBeenCalledWith(error, context)
})

it('does nothing when catch is not a function', () => {
  // @ts-expect-error: intentionally wrong shape
  handleNotAwaitedPromise({ catch: 123 })
  expect(captureException).not.toHaveBeenCalled()
})

Want me to push these tests?

packages/react-kitchen-sink/src/sentry/__tests__/sentrySchemaQuerySnapshotListener.test.ts (5)

1-12: Reset mocks between tests to avoid cross-test leakage

Add afterEach(() => vi.clearAllMocks()) to keep tests isolated if/when more tests are added.

-import { describe, expect, it, vi } from 'vitest'
+import { describe, expect, it, vi, afterEach } from 'vitest'
@@
 vi.mock('@sentry/react', () => ({
   addBreadcrumb: vi.fn(),
   captureException: vi.fn(),
 }))
 
+afterEach(() => {
+  vi.clearAllMocks()
+})

23-34: Assert the forwarded state and breadcrumb payloads, not just that calls occurred

Strengthen assertions to verify the exact state forwarding and breadcrumb content. This catches accidental regressions in message/category/payload.

-    listener.onSnapshot?.({
-      isLoading: false,
-      hasError: false,
-      disabled: false,
-      empty: true,
-      size: 0,
-      data: [],
-      snapshot: stub<QuerySnapshot>(),
-    } as const)
-    expect(onSnapshot).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    const state = {
+      isLoading: false,
+      hasError: false,
+      disabled: false,
+      empty: true,
+      size: 0,
+      data: [],
+      snapshot: stub<QuerySnapshot>(),
+    } as const
+    listener.onSnapshot?.(state)
+    expect(onSnapshot).toHaveBeenCalledWith(state)
+    expect(addBreadcrumb).toHaveBeenNthCalledWith(
+      1,
+      expect.objectContaining({
+        category: 'firestore',
+        message: `received query snapshot for (${query.name})`,
+        data: { state },
+      }),
+    )

35-39: Verify captureException context (tags, fingerprint, contexts.query) to match implementation

Asserting the capture context prevents accidental loss of helpful Sentry metadata.

-    expect(captureException).toHaveBeenCalled()
+    expect(captureException).toHaveBeenCalledWith(
+      error,
+      expect.objectContaining({
+        tags: expect.objectContaining({ firestore: true, 'schema.collection': 'c' }),
+        fingerprint: ['firestore', 'c'],
+        contexts: { query: { query } },
+      }),
+    )

40-43: Check the closing breadcrumb content and call order

This ensures the “closed” breadcrumb is emitted, with the right message and as the second breadcrumb.

-    expect(addBreadcrumb).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenNthCalledWith(
+      2,
+      expect.objectContaining({
+        category: 'firestore',
+        message: `closed query snapshot for (${query.name})`,
+      }),
+    )

4-4: Optional: remove vitest-mock-extended to avoid the extra dev dependency

If you prefer to keep the test lightweight and avoid the no-unresolved on vitest-mock-extended, you can replace mock/stub usage with plain values. Note: casting query to any preserves compatibility with the listener’s expected type.

-import { mock, stub } from 'vitest-mock-extended'
+// (optional) remove vitest-mock-extended usage

-  const query = mock<{ name: string }>()
-  query.name = 'q'
+  const query = { name: 'q' } as any
@@
-      snapshot: stub<QuerySnapshot>(),
+      snapshot: undefined as unknown as QuerySnapshot,

Also applies to: 14-15, 30-30

packages/react-kitchen-sink/src/sentry/__tests__/sentryDocumentSnapshotListener.test.ts (1)

30-41: Strengthen assertions to reduce false positives

Assert argument shapes and call counts to lock in the Sentry payloads and breadcrumb messages.

-    expect(onSnapshot).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    expect(onSnapshot).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenCalledTimes(1)
+    expect(addBreadcrumb).toHaveBeenLastCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'received document snapshot for (/c/id)',
+      }),
+    )
@@
-    expect(captureException).toHaveBeenCalled()
+    expect(captureException).toHaveBeenCalledWith(
+      error,
+      expect.objectContaining({
+        tags: expect.objectContaining({ firestore: true, 'schema.collection': 'c' }),
+        fingerprint: ['firestore', 'c'],
+      }),
+    )
@@
-    expect(onComplete).toHaveBeenCalled()
-    expect(addBreadcrumb).toHaveBeenCalled()
+    expect(onComplete).toHaveBeenCalled()
+    expect(addBreadcrumb).toHaveBeenCalledTimes(2)
+    expect(addBreadcrumb).toHaveBeenLastCalledWith(
+      expect.objectContaining({
+        category: 'firestore',
+        message: 'closed document snapshot for (/c/id)',
+      }),
+    )
packages/react-kitchen-sink/src/sentry/__tests__/processErrorWithSentryCaptureContext.test.ts (1)

2-7: Optional: restore spies after each test

Add afterEach(vi.restoreAllMocks) to keep tests isolated.

-import { describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
@@
 
 import { ErrorWithSentryCaptureContext } from '../ErrorWithSentryCaptureContext'
 import { processErrorWithSentryCaptureContext } from '../processErrorWithSentryCaptureContext'
 
+afterEach(() => {
+  vi.restoreAllMocks()
+})
packages/react-kitchen-sink/src/sentry/__tests__/ErrorWithSentryCaptureContext.test.ts (1)

2-6: Optional: restore spies after each test

Add afterEach(vi.restoreAllMocks) for isolation across tests.

-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
@@
 import { ErrorWithSentryCaptureContext } from '../ErrorWithSentryCaptureContext'
 
+afterEach(() => {
+  vi.restoreAllMocks()
+})
packages/react-kitchen-sink/src/schema/firestoreDataConverter.ts (3)

22-24: LGTM on the generic signature; add an explicit return type for clarity

The expanded <AppModelType, DbModelType extends DocumentData> signature aligns well with Firestore’s typed snapshots. Consider adding an explicit return type to make intent unambiguous and help inference at call sites.

-export const firestoreDataConverter = <AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
-  snapshot: QueryDocumentSnapshot<AppModelType, DbModelType>,
-) => handleFirestoreDataTypes(snapshot.data())
+export const firestoreDataConverter = <
+  AppModelType = DocumentData,
+  DbModelType extends DocumentData = DocumentData
+>(
+  snapshot: QueryDocumentSnapshot<AppModelType, DbModelType>,
+): AppModelType => handleFirestoreDataTypes(snapshot.data())

15-16: Avoid converting non-plain objects; restrict recursion to POJOs

value instanceof Object will recurse into any object (including Firestore DocumentReference or custom class instances), potentially mutating types that should be preserved. Restrict to plain objects.

-    case value instanceof Object:
-      return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, handleFirestoreDataTypes(v)])) as T
+    case value !== null && typeof value === 'object' && (value as object).constructor === Object:
+      return Object.fromEntries(
+        Object.entries(value as Record<string, unknown>).map(([key, v]) => [key, handleFirestoreDataTypes(v)]),
+      ) as T

1-1: Review filename-case rule enforcement and Firebase import resolution

  • Verified that react-kitchen-sink declares firebase@^12.0.0 in packages/react-kitchen-sink/package.json, so
    import … from 'firebase/firestore' will resolve correctly.
  • No ESLint config (.eslintrc.* or eslint.config.js) enforcing unicorn/filename-case was found in the repo.
    If you’re using a shared or workspace-level ESLint setup that does enforce kebab-case filenames, please confirm its location and rules.
  • If unicorn/filename-case is enabled, rename this file:
    • packages/react-kitchen-sink/src/schema/firestoreDataConverter.ts → firestore-data-converter.ts

Otherwise, the import and filename can remain as-is.

packages/react-kitchen-sink/src/schema/__tests__/firestoreDataConverter.test.ts (1)

6-6: Update import path if you rename the implementation file to kebab-case

If you adopt kebab-case for the source file, adjust this import accordingly.

-import { firestoreDataConverter } from '../firestoreDataConverter'
+import { firestoreDataConverter } from '../firestore-data-converter'
packages/react-kitchen-sink/src/rxjs/__tests__/documentSnapshotSubject.test.ts (3)

8-14: Ensure the mock is applied before importing the module under test

To guarantee documentSnapshotSubject uses the mocked @valian/rxjs-firebase implementation, register the mock before importing ../documentSnapshotSubject. This avoids leaking real implementations in ESM environments.

Apply this diff to hoist the mock and import the SUT after:

-6 import { documentSnapshotSubject } from '../documentSnapshotSubject'
-
-8 vi.mock('@valian/rxjs-firebase', () => ({
+8 vi.mock('@valian/rxjs-firebase', () => ({
   fromDocumentRef: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
   DocumentSnapshotSubject: class {
     close = vi.fn()
     subscribe = vi.fn()
   },
 }))
+
+import { documentSnapshotSubject } from '../documentSnapshotSubject'

Alternatively, keep imports as-is and switch to a dynamic import after mocking:

vi.mock('@valian/rxjs-firebase', /* factory */)
const { documentSnapshotSubject } = await import('../documentSnapshotSubject')

Also applies to: 6-6


1-1: Rename file to kebab-case to satisfy ESLint (unicorn/filename-case)

Rename to: document-snapshot-subject.test.ts


2-2: ESLint “Unable to resolve path” — configure import resolvers for workspace packages

If ESLint still reports unresolved imports for @valian/rxjs-firebase or vitest-mock-extended, configure eslint-plugin-import resolvers for TypeScript and node to include workspace packages.

Add to your ESLint config:

settings: {
  'import/resolver': {
    typescript: { project: ['./packages/*/tsconfig.json'] },
    node: { extensions: ['.ts', '.tsx', '.js', '.mjs'] },
  },
},

Also applies to: 4-4

packages/react-kitchen-sink/package.json (1)

62-64: Avoid duplicating zustand in devDependencies and optionalDependencies

Having zustand in both is redundant. Keep it only as optional if runtime-optional, or as a peer if required by consumers.

Suggested change (keep as optional only):

   "devDependencies": {
-    "vitest-mock-extended": "^3.1.0",
+    "vitest-mock-extended": "^3.1.0",
     "@sentry/core": "^10.1.0",
     "@sentry/react": "^10.1.0",
     "@tanstack/react-query": "^5.84.2",
     "firebase": "^12.0.0",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "rxjs": "^7.8.2",
     "zod": "^4.0.17",
     "zod-firebase": "^2.0.0",
-    "zustand": "^5"
+    // "zustand": "^5" // remove here if kept as optional
   },
@@
   "optionalDependencies": {
     "zustand": "^5"
   },

Also applies to: 39-50

packages/react-kitchen-sink/src/react-query/__tests__/documentSnapshotQueryOptions.test.ts (1)

1-1: Rename file to kebab-case (unicorn/filename-case)

Rename to: document-snapshot-query-options.test.ts

packages/react-kitchen-sink/src/rxjs/__tests__/schemaQuerySnapshotSubject.test.ts (2)

8-14: Mock before importing the SUT to guarantee isolation

Move the vi.mock('@valian/rxjs-firebase', ...) block before importing ../schemaQuerySnapshotSubject to ensure the SUT uses the mocked fromQuery and QuerySnapshotSubject.

Apply this diff:

-6 import { schemaQuerySnapshotSubject } from '../schemaQuerySnapshotSubject'
-
-8 vi.mock('@valian/rxjs-firebase', () => ({
+8 vi.mock('@valian/rxjs-firebase', () => ({
   fromQuery: vi.fn().mockReturnValue({ subscribe: vi.fn() }),
   QuerySnapshotSubject: class {
     close = vi.fn()
     subscribe = vi.fn()
   },
 }))
+
+import { schemaQuerySnapshotSubject } from '../schemaQuerySnapshotSubject'

Also applies to: 6-6


1-1: Rename file to kebab-case

Rename to: schema-query-snapshot-subject.test.ts

packages/react-kitchen-sink/src/react-query/__tests__/querySnapshotQueryOptions.test.ts (1)

1-1: Rename file to kebab-case

Rename to: query-snapshot-query-options.test.ts

packages/react-kitchen-sink/src/react-query/observers/__tests__/documentSnapshotQueryClientObserver.test.ts (3)

16-18: Assert exact payload passed to setQueryData on next()

Strengthen the test by asserting the exact state passed through to QueryClient rather than a loose “called” assertion.

Apply this diff:

-    observer.next({ isLoading: false, hasError: false, disabled: false } as DocumentSnapshotState)
-    expect(client.setQueryData).toHaveBeenCalled()
+    const state = { isLoading: false, hasError: false, disabled: false } as DocumentSnapshotState
+    observer.next(state)
+    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, state)

8-8: Verify Sentry capture on error path

You’re mocking Sentry but not asserting it’s invoked. Add an assertion and a type-only import to ensure we don’t regress error reporting.

Apply these diffs:

+import { captureException } from '@sentry/react'
     observer.error(new Error('x'))
+    expect(captureException).toHaveBeenCalledWith(expect.any(Error))
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       isLoading: false,
       hasError: true,
       disabled: false,
     })

Also applies to: 19-24


26-32: Also assert total call count for setQueryData

This ensures no extra/unexpected cache writes happen.

Apply this diff:

     observer.complete()
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       isLoading: false,
       hasError: false,
       disabled: true,
     })
+    expect(client.setQueryData).toHaveBeenCalledTimes(3)
packages/react-kitchen-sink/src/react-query/__tests__/schemaQuerySnapshotQueryOptions.test.ts (4)

1-1: Import Firestore types from public module

Avoid internal @firebase/* imports; use firebase/firestore for type-only imports.

Apply this diff:

-import { type SnapshotListenOptions } from '@firebase/firestore'
+import { type SnapshotListenOptions } from 'firebase/firestore'

3-3: Use the schema-specific listener type to match factory API

schemaQuerySnapshotSubjectFactory expects SchemaQuerySnapshotStateListener; aligning the test’s listener type reduces the chance of drift.

Apply these diffs:

-import { type QuerySnapshotStateListener } from '@valian/rxjs-firebase'
+import { type SchemaQuerySnapshotStateListener } from '../../rxjs/types'
-    const listener = mock<QuerySnapshotStateListener>()
+    const listener = mock<SchemaQuerySnapshotStateListener<any, any>>()

If you intentionally unified listener types across schema/non-schema paths, feel free to skip; otherwise this better matches FirestoreSnapshotManager.schemaQuerySnapshotSubjectFactory’s signature.

Also applies to: 24-24


33-41: Broaden assertions: ensure meta is fully populated

You can assert meta fields to guard against regressions in the options builder.

Apply this diff:

     expect(opts.queryKey).toEqual(queryOptions({ queryKey: ['k'] }).queryKey)
     expect(snapshotManager.schemaQuerySnapshotSubjectFactory).toHaveBeenCalledWith(
       collections.users,
       { name: 'q' },
       snapshotOptions,
       listener,
     )
-    expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.type).toBe('snapshot')
+    expect(opts.meta?.snapshotManager).toBe(snapshotManager)
+    expect(opts.meta?.collection).toBe(collections.users.collectionName)
+    expect(opts.meta?.schemaQuery).toEqual({ name: 'q' })
+    expect(opts.staleTime).toBe(Infinity)
+    expect(opts.retry).toBe(false)
+    expect(opts.gcTime).toBe(10_000)

1-7: Resolve ESLint “unresolved import” and filename-case warnings

  • Ensure devDependencies exist for all test-time imports (firebase, @tanstack/react-query, @valian/rxjs-firebase, zod, zod-firebase, vitest-mock-extended).
  • Rename the file to kebab-case as per the rule.

You can reuse the dependency check script provided in the other comment, and consider renaming:

  • schemaQuerySnapshotQueryOptions.test.ts -> schema-query-snapshot-query-options.test.ts
packages/react-kitchen-sink/src/react-query/queryFn/__tests__/queryFnFromDocumentSnapshotSubjectFactory.test.ts (5)

1-1: Import Firestore types from public module

Use firebase/firestore rather than internal @firebase/* package.

Apply this diff:

-import { type DocumentSnapshot } from '@firebase/firestore'
+import { type DocumentSnapshot } from 'firebase/firestore'

32-33: Pass a queryKey in all QueryFunctionContext mocks for consistency

A few tests omit queryKey; while they pass today, providing it makes behavior consistent and guards against future usage changes.

Apply these diffs:

-    const result = await queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const result = await queryFn(
+      mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey: ['k'] }),
+    )
-    const promise = queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const promise = queryFn(
+      mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey: ['k'] }),
+    )

Also applies to: 42-43


96-107: Consider closing/unsubscribing the subject on waitForData timeout

Currently, a timeout leads to a rejected promise but the subscription remains active (until abort or GC), which can leak resources and keep pushing cache updates to a failed query.

If this is unintended, adjust the production code to close on timeout/error for the waitForData branch. For example, in queryFnFromDocumentSnapshotSubjectFactory.ts:

// Pseudocode patch in queryFnFromDocumentSnapshotSubjectFactory.ts waitForData branch
try {
  return await firstValueFrom(
    subject$.pipe(
      takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
      skipWhile(({ isLoading }) => isLoading),
    ),
  )
} catch (err) {
  subscription.unsubscribe()
  subject$.close()
  throw err
}

I can prep a similar change for the query snapshot variant if you want to apply it consistently.


109-131: Use fake timers to reduce flakiness in timeout-based tests

Relying on setTimeout(0) can be flaky under load. Fake timers make these tests deterministic and faster.

Example pattern (no diff due to multiple insert points):

  • At the start of the relevant tests: vi.useFakeTimers()
  • Schedule emissions, then vi.runAllTimers() or vi.advanceTimersByTime(timeout)
  • After each test: vi.useRealTimers()

1-7: Resolve ESLint import and filename-case warnings

  • Ensure devDependencies exist for firebase, @tanstack/react-query, @valian/rxjs-firebase, rxjs, vitest-mock-extended.
  • Rename file to kebab-case: query-fn-from-document-snapshot-subject-factory.test.ts.

Reuse the dependency check script from the earlier comment.

packages/react-kitchen-sink/src/react-query/observers/__tests__/querySnapshotQueryClientObserver.test.ts (3)

16-24: Assert exact payload passed to setQueryData on next()

Mirror the stronger assertion used elsewhere by checking the exact state.

Apply this diff:

-    observer.next({
+    const state = {
       empty: false,
       size: 1,
       isLoading: false,
       hasError: false,
       disabled: false,
       data: [{}],
-    } as QuerySnapshotState)
-    expect(client.setQueryData).toHaveBeenCalled()
+    } as QuerySnapshotState
+    observer.next(state)
+    expect(client.setQueryData).toHaveBeenCalledWith(queryKey, state)

8-8: Verify Sentry capture on error path

You already mock Sentry; assert that captureException is called when error() is invoked.

Apply these diffs:

+import { captureException } from '@sentry/react'
     observer.error(new Error('x'))
+    expect(captureException).toHaveBeenCalledWith(expect.any(Error))
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       empty: true,
       size: 0,
       isLoading: false,
       hasError: true,
       disabled: false,
       data: [],
     })

Also applies to: 26-35


36-45: Also assert total call count for setQueryData

Ensure only the intended three cache writes occur.

Apply this diff:

     observer.complete()
     expect(client.setQueryData).toHaveBeenCalledWith(queryKey, {
       empty: true,
       size: 0,
       isLoading: false,
       hasError: false,
       disabled: true,
       data: [],
     })
+    expect(client.setQueryData).toHaveBeenCalledTimes(3)
packages/react-kitchen-sink/src/react-query/queryFn/__tests__/queryFnFromQuerySnapshotSubjectFactory.test.ts (5)

1-1: Import Firestore types from public module

Prefer firebase/firestore over @firebase/*.

Apply this diff:

-import { type QuerySnapshot } from '@firebase/firestore'
+import { type QuerySnapshot } from 'firebase/firestore'

39-40: Provide queryKey in all QueryFunctionContext mocks

Keep tests consistent and future-proof by always including queryKey.

Apply these diffs:

-    const result = await queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const result = await queryFn(
+      mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey: ['k'] }),
+    )
-    const promise = queryFn(mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient }))
+    const promise = queryFn(
+      mock<QueryFunctionContext>({ signal: controller.signal, client: queryClient, queryKey: ['k'] }),
+    )

Also applies to: 56-57


122-133: Consider closing/unsubscribing the subject on waitForData timeout

Same rationale as the document variant: avoid keeping live subscriptions after a rejected promise due to timeout.

Proposed change (in queryFnFromQuerySnapshotSubjectFactory.ts) for the waitForData branch:

try {
  return await firstValueFrom(
    subject$.pipe(
      takeUntil(timer(options.waitForDataTimeout ?? 10_000)),
      skipWhile(({ isLoading }) => isLoading),
    ),
  )
} catch (err) {
  subscription.unsubscribe()
  subject$.close()
  throw err
}

135-153: Use fake timers to stabilize timer-driven tests

Swap real timers for vi.useFakeTimers() to make timeout tests deterministic and fast.

Pattern:

  • vi.useFakeTimers() at test start
  • schedule subject.next(...)
  • vi.runAllTimers() or vi.advanceTimersByTime(ms)
  • vi.useRealTimers() in afterEach

1-7: Resolve ESLint import and filename-case warnings

  • Ensure devDependencies for firebase, @tanstack/react-query, @valian/rxjs-firebase, rxjs, vitest-mock-extended are present.
  • Rename to kebab-case: query-fn-from-query-snapshot-subject-factory.test.ts.

Reuse the earlier script to validate package.json entries.

packages/react-kitchen-sink/src/react-query/__tests__/FirestoreSnapshotManager.test.ts (3)

39-47: Reset mocks between tests to avoid cross-test leakage

Minor improvement: clear all mock call histories in beforeEach for repeatable assertions.

Apply this diff:

   beforeEach(() => {
+    vi.clearAllMocks()
     subscribers.length = 0
     const queryCache = mock<QueryCache>()
     queryClient.getQueryCache.mockReturnValue(queryCache)

96-114: Fix typo in test name

Double “schema” in the test description is a nit.

Apply this diff:

-  it('schema schemaDocumentSnapshotSubjectFactory produce subjects and register closures', () => {
+  it('schemaDocumentSnapshotSubjectFactory produces subjects and registers closures', () => {

1-1: Silence known ESLint false-positives for tests (filename-case, max-classes-per-file)

Given the linter hints and that this is a test file with tiny mock classes, you can pragmatically disable these rules here.

Apply this diff at the very top of the file:

+/* eslint-disable unicorn/filename-case, max-classes-per-file */
 import { type DocumentReference, type Query } from '@firebase/firestore'
packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromQuerySnapshotSubjectFactory.ts (3)

33-40: Close the subject before unsubscribing so the observer receives “complete” (disabled=true)

Calling close() first lets QuerySnapshotSubject complete, which in turn triggers the query client observer’s complete handler to stamp disabled: true into the cache. Then unsubscribe to detach the observer. Current order drops the complete notification.

Apply this diff:

 signal.addEventListener(
   'abort',
   () => {
-    subscription.unsubscribe()
-    subject$.close()
+    subject$.close()
+    subscription.unsubscribe()
   },
   { once: true },
 )

19-28: Optional: surface cancellation via an AbortError instead of returning a disabled state

React Query treats thrown AbortError as a canceled fetch; returning a “disabled” state is a valid design choice too. If you prefer canonical cancellation semantics, throw instead.

Apply this diff:

-    if (signal.aborted) {
-      return {
-        empty: true,
-        size: 0,
-        isLoading: false,
-        hasError: false,
-        disabled: true,
-        data: [],
-      } as const
-    }
+    if (signal.aborted) {
+      throw new DOMException('Aborted', 'AbortError')
+    }

If you keep the current behavior for consistency with the rest of the API, consider documenting it in the options JSDoc.


42-49: Timeout path will reject the query; confirm this is intentional

When waitForData is true, takeUntil(timer(...)) completes the stream without emitting if data never arrives, causing firstValueFrom to reject. That will mark the query as errored. If the intended UX is to remain in loading or return a non-error sentinel, consider handling the timeout differently (e.g., returning an initial state).

packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromDocumentSnapshotSubjectFactory.ts (3)

30-37: Close-before-unsubscribe for consistent “complete” propagation

Same rationale as the query snapshot variant: closing first lets the observer receive complete and mark the cache disabled.

Apply this diff:

 signal.addEventListener(
   'abort',
   () => {
-    subscription.unsubscribe()
-    subject$.close()
+    subject$.close()
+    subscription.unsubscribe()
   },
   { once: true },
 )

19-25: Optional: consider throwing AbortError instead of returning a disabled state

Returning a disabled state is consistent with your snapshot pattern; alternatively, throwing an AbortError signals cancellation to React Query.

Apply this diff:

-    if (signal.aborted) {
-      return {
-        isLoading: false,
-        hasError: false,
-        disabled: true,
-      } as const
-    }
+    if (signal.aborted) {
+      throw new DOMException('Aborted', 'AbortError')
+    }

39-46: Timeout behavior will reject; verify desired UX

If waitForData times out, firstValueFrom will reject. Ensure consumers expect an error state vs. continuing to load or returning a sentinel value.

packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (2)

68-71: Pass only the needed options to the queryFn factory.

Passing the whole props object is safe but noisy. Limiting to the factory’s option shape clarifies intent and avoids accidental coupling.

-    queryFn: queryFnFromDocumentSnapshotSubjectFactory(
-      snapshotManager.documentSnapshotSubjectFactory(ref, snapshotOptions, listener),
-      props,
-    ),
+    queryFn: queryFnFromDocumentSnapshotSubjectFactory(
+      snapshotManager.documentSnapshotSubjectFactory(ref, snapshotOptions, listener),
+      {
+        waitForData: props.waitForData,
+        waitForDataTimeout: props.waitForDataTimeout,
+      },
+    ),

42-50: Align input key typing with DataTag for stronger inference.

The result type brands queryKey as DataTag<...>, but the input accepts any QueryKey. Consider requiring the input queryKey to already be DataTag-typed to guarantee type-level alignment and better inference downstream. If that’s too strict for callers, keep as-is.

packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (2)

77-80: Limit options passed to the queryFn factory to its shape.

Mirror the document variant: only pass waitForData/waitForDataTimeout.

-    queryFn: queryFnFromQuerySnapshotSubjectFactory(
-      snapshotManager.schemaQuerySnapshotSubjectFactory(factory, query, snapshotOptions, listener),
-      props,
-    ),
+    queryFn: queryFnFromQuerySnapshotSubjectFactory(
+      snapshotManager.schemaQuerySnapshotSubjectFactory(factory, query, snapshotOptions, listener),
+      {
+        waitForData: props.waitForData,
+        waitForDataTimeout: props.waitForDataTimeout,
+      },
+    ),

50-57: Consider requiring a DataTag queryKey at input for stronger typing.

Like the document builder, the result brands the key, but the input accepts any QueryKey. Consider tightening the input to require a DataTag-branded key to improve type safety.

Comment thread packages/react-kitchen-sink/package.json
Comment thread packages/react-kitchen-sink/README.md Outdated
@Nr9 Nr9 enabled auto-merge (squash) August 16, 2025 16:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (1)

2-8: ESLint unresolved imports (duplicate of prior feedback).

This repeats the earlier issue about unresolved '@tanstack/react-query' and '@valian/rxjs-firebase' imports due to ESLint resolver configuration in a monorepo/TS workspace. Please refer to the previous guidance to adjust the import resolver and devDeps.

Also applies to: 9-9

packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (1)

2-14: Resolve ESLint “unresolved module” for @tanstack/react-query and zod-firebase

These imports are flagged by ESLint. This is a workspace resolver issue (not a code issue). Please ensure ESLint’s TypeScript resolver is configured to include all workspace tsconfig files.

Run this to verify dependency declarations and resolver config presence:

#!/bin/bash
set -euo pipefail

echo "== Deps declared in package.json =="
fd -t f package.json -E node_modules | while read -r f; do
  tanstack=$(jq -r '(.dependencies["@tanstack/react-query"] // .devDependencies["@tanstack/react-query"] // empty)' "$f")
  zodfb=$(jq -r '(.dependencies["zod-firebase"] // .devDependencies["zod-firebase"] // empty)' "$f")
  if [[ -n "$tanstack" || -n "$zodfb" ]]; then
    echo "-> $f"
    [[ -n "$tanstack" ]] && echo "   @tanstack/react-query: $tanstack"
    [[ -n "$zodfb"    ]] && echo "   zod-firebase: $zodfb"
  fi
done

echo
echo "== ESLint resolver config =="
rg -nC2 -S "import/resolver" -g "!.*/node_modules/**" --glob ".eslintrc.*" || true
rg -nC2 -S "parserOptions.*project" -g "!.*/node_modules/**" --glob ".eslintrc.*" || true

echo
echo "Tip: Ensure settings.import/resolver.typescript.project includes both './tsconfig.json' and './packages/*/tsconfig.json'."
🧹 Nitpick comments (5)
packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (1)

68-71: Pass only the recognized queryFn options instead of the entire props.

queryFnFromDocumentSnapshotSubjectFactory only consumes waitForData and waitForDataTimeout. Passing the entire props object is looser than necessary and obscures intent.

Apply this diff:

     queryFn: queryFnFromDocumentSnapshotSubjectFactory(
       snapshotManager.documentSnapshotSubjectFactory(ref, snapshotOptions, listener),
-      props,
+      {
+        waitForData: props.waitForData,
+        waitForDataTimeout: props.waitForDataTimeout,
+      },
     ),
packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (2)

24-48: Unify queryKey typing: require DataTag on input as well to match the result type

The result interface narrows queryKey to a DataTag, but the input options accept a plain QueryKey. Tightening the input to DataTag improves type-safety and aligns with your invalidation/tagging strategy.

Apply:

 export interface SchemaQuerySnapshotQueryOptions<
   TCollectionSchema extends CollectionSchema,
   TOptions extends MetaOutputOptions,
   TError = DefaultError,
   TData = SchemaQuerySnapshotState<TCollectionSchema, TOptions>,
   TQueryKey extends QueryKey = QueryKey,
 > extends Omit<
-      UnusedSkipTokenOptions<SchemaQuerySnapshotState<TCollectionSchema, TOptions>, TError, TData, TQueryKey>,
+      UnusedSkipTokenOptions<SchemaQuerySnapshotState<TCollectionSchema, TOptions>, TError, TData, TQueryKey>,
       | 'queryFn'
+      | 'queryKey'
       | 'initialData'
       | 'staleTime'
       | 'refetchInterval'
       | 'refetchIntervalInBackground'
       | 'refetchOnWindowFocus'
       | 'refetchOnMount'
       | 'refetchOnReconnect'
       | 'retryOnMount'
       | 'retry'
     >,
     QueryFnFromQuerySnapshotSubjectFactoryOptions {
+  queryKey: DataTag<TQueryKey, SchemaQuerySnapshotState<TCollectionSchema, TOptions>, TError>
   factory: SchemaFirestoreQueryFactory<TCollectionSchema>
   query: SchemaQuerySpecification<TCollectionSchema, TOptions>
   snapshotOptions?: TOptions & SnapshotListenOptions
   listener?: SchemaQuerySnapshotStateListener<TCollectionSchema, TOptions>
 }

67-75: Avoid passing the entire props object into queryFn factory (reduce coupling/accidental leakage)

Limit the options passed to the queryFn factory to the specific fields it understands to avoid accidental coupling if more fields are added to props later.

Apply:

 export const schemaQuerySnapshotQueryOptions = <
@@
 >(
   snapshotManager: FirestoreSnapshotManager,
   {
     factory,
     query,
     snapshotOptions,
     listener,
-    ...props
+    waitForData,
+    waitForDataTimeout,
+    ...rest
   }: SchemaQuerySnapshotQueryOptions<TCollectionSchema, TOptions, TError, TData, TQueryKey>,
 ): SchemaQuerySnapshotQueryOptionsResult<TCollectionSchema, TOptions, TError, TData, TQueryKey> =>
   queryOptions({
-    queryFn: queryFnFromQuerySnapshotSubjectFactory(
-      snapshotManager.schemaQuerySnapshotSubjectFactory(factory, query, snapshotOptions, listener),
-      props,
-    ),
+    queryFn: queryFnFromQuerySnapshotSubjectFactory(
+      snapshotManager.schemaQuerySnapshotSubjectFactory(factory, query, snapshotOptions, listener),
+      { waitForData, waitForDataTimeout },
+    ),
     staleTime: Infinity,
     retry: false,
     gcTime: 10_000,
-    ...props,
+    ...rest,
     meta: {
-      ...props.meta,
+      ...rest.meta,
       type: 'snapshot',
       snapshotManager,
       collection: factory.collectionName,
       schemaQuery: query,
     },
   })
packages/react-kitchen-sink/src/react-query/querySnapshotQueryOptions.ts (2)

68-71: Pass only factory options to the queryFn factory (avoid leaking unrelated props).

Narrow the second argument to just the factory options to prevent accidental coupling with future keys in props.

-    queryFn: queryFnFromQuerySnapshotSubjectFactory(
-      snapshotManager.querySnapshotSubjectFactory(query, snapshotOptions, listener),
-      props,
-    ),
+    queryFn: queryFnFromQuerySnapshotSubjectFactory(
+      snapshotManager.querySnapshotSubjectFactory(query, snapshotOptions, listener),
+      { waitForData: props.waitForData, waitForDataTimeout: props.waitForDataTimeout },
+    ),

72-76: Default gcTime (10s) may be too aggressive for snapshot queries.

With live snapshots, a short gcTime can cause frequent teardown/re-create cycles when components mount/unmount rapidly. It’s overrideable via props, but consider a higher default (e.g., 60s) or document the trade-offs.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 42d563d and 2b19260.

📒 Files selected for processing (3)
  • packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (1 hunks)
  • packages/react-kitchen-sink/src/react-query/querySnapshotQueryOptions.ts (1 hunks)
  • packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (3)
packages/react-kitchen-sink/src/rxjs/index.ts (2)
  • DocumentSnapshotState (5-5)
  • DocumentSnapshotStateListener (6-6)
packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromDocumentSnapshotSubjectFactory.ts (2)
  • QueryFnFromDocumentSnapshotSubjectFactoryOptions (8-11)
  • queryFnFromDocumentSnapshotSubjectFactory (13-53)
packages/react-kitchen-sink/src/react-query/FirestoreSnapshotManager.ts (1)
  • FirestoreSnapshotManager (37-129)
packages/react-kitchen-sink/src/react-query/querySnapshotQueryOptions.ts (3)
packages/react-kitchen-sink/src/rxjs/index.ts (2)
  • QuerySnapshotState (7-7)
  • QuerySnapshotStateListener (8-8)
packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromQuerySnapshotSubjectFactory.ts (2)
  • QueryFnFromQuerySnapshotSubjectFactoryOptions (8-11)
  • queryFnFromQuerySnapshotSubjectFactory (13-59)
packages/react-kitchen-sink/src/react-query/FirestoreSnapshotManager.ts (1)
  • FirestoreSnapshotManager (37-129)
packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (3)
packages/react-kitchen-sink/src/rxjs/types.ts (2)
  • SchemaQuerySnapshotState (21-24)
  • SchemaQuerySnapshotStateListener (44-50)
packages/react-kitchen-sink/src/react-query/queryFn/queryFnFromQuerySnapshotSubjectFactory.ts (2)
  • QueryFnFromQuerySnapshotSubjectFactoryOptions (8-11)
  • queryFnFromQuerySnapshotSubjectFactory (13-59)
packages/react-kitchen-sink/src/react-query/FirestoreSnapshotManager.ts (1)
  • FirestoreSnapshotManager (37-129)
🪛 ESLint
packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts

[error] 8-8: Unable to resolve path to module '@tanstack/react-query'.

(import-x/no-unresolved)


[error] 9-9: Unable to resolve path to module '@valian/rxjs-firebase'.

(import-x/no-unresolved)

packages/react-kitchen-sink/src/react-query/querySnapshotQueryOptions.ts

[error] 8-8: Unable to resolve path to module '@tanstack/react-query'.

(import-x/no-unresolved)


[error] 9-9: Unable to resolve path to module '@valian/rxjs-firebase'.

(import-x/no-unresolved)

packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts

[error] 8-8: Unable to resolve path to module '@tanstack/react-query'.

(import-x/no-unresolved)


[error] 14-14: Unable to resolve path to module 'zod-firebase'.

(import-x/no-unresolved)

🔇 Additional comments (6)
packages/react-kitchen-sink/src/react-query/documentSnapshotQueryOptions.ts (1)

76-81: Good: snapshot meta fields are authoritative.

Spreading props.meta first and then setting type/snapshotManager/documentRef ensures callers can’t override critical cleanup metadata. This addresses the earlier leak risk.

packages/react-kitchen-sink/src/react-query/schemaQuerySnapshotQueryOptions.ts (2)

76-84: Sane defaults for snapshot-backed queries (staleTime, retry, gcTime)

Defaults make sense for live snapshot streams: never stale, don’t retry, and a short GC to release subscriptions promptly on unmount. No changes requested.


85-91: meta.type is locked to 'snapshot' (prevents subscription leaks) — resolved

Good fix placing spread before type so callers cannot override meta.type.

packages/react-kitchen-sink/src/react-query/querySnapshotQueryOptions.ts (3)

78-82: Great fix: meta ordering now prevents overriding type: 'snapshot'.

Spreading props.meta first and then forcing type, snapshotManager, and query ensures cleanup can’t be broken by callers. This closes the leakage hole raised previously.


36-36: Inconsistency with summary: waitForData/waitForDataTimeout are still public.

The code extends QueryFnFromQuerySnapshotSubjectFactoryOptions, so these options remain in the public API, contrary to the AI summary’s note about removal. Confirm that keeping them exposed is intentional.


42-50: No changes needed for DataTag usage
Confirmed that @tanstack/react-query v5 re-exports the DataTag type from @tanstack/query-core and that it correctly composes with UnusedSkipTokenOptions. The queryOptions(...) helpers produce a DataTag-narrowed queryKey, so the return type is assignable to QuerySnapshotQueryOptionsResult<…>.

@Nr9 Nr9 disabled auto-merge August 16, 2025 16:31
@Nr9 Nr9 merged commit b3f4722 into main Aug 16, 2025
6 of 7 checks passed
@Nr9 Nr9 deleted the feat/react-query branch August 16, 2025 16:31
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/react-kitchen-sink/README.md (3)

26-26: Peer dependency aligned to zod-firebase v2 — good catch

Thanks for updating the peer dependency to zod-firebase ^2 to match the examples.


32-32: Typo fixed: FirestoreSnapshotManager

Appreciate correcting “Snaphot” → “Snapshot” in the public docs.


35-39: Fix imports in the React Query example: remove unused and add missing

  • Remove unused: QueryClient, doc, and db.
  • Add missing: z (from zod) and the collection builder (imported here as collectionsBuilder) to match usage below.

These omissions will break copy/paste usage of the snippet.

Apply this diff:

-import { QueryClient, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
-import { doc } from 'firebase/firestore'
-import { db } from './firebase'
-import { documentSnapshotQueryOptions, FirestoreSnapshotManager } from '@valian/react-kitchen-sink/react-query'
+import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
+import { documentSnapshotQueryOptions, FirestoreSnapshotManager } from '@valian/react-kitchen-sink/react-query'
+import { z } from 'zod'
+import { collectionsBuilder } from 'zod-firebase'
🧹 Nitpick comments (3)
packages/react-kitchen-sink/README.md (3)

43-45: Schema/usage mismatch: email referenced but not in schema

You render data.data?.email but UserZod doesn’t define email. Either remove the field from the JSX or add it to the schema (optional is fine).

Apply this diff to add an optional, validated email:

 const UserZod = z.object({
   name: z.string(),
+  email: z.string().email().optional(),
 })

Also applies to: 64-64


99-101: Prefer stable keys over array index in lists

Use a stable identifier (e.g., Firestore doc id) instead of i to avoid unnecessary re-renders and subtle bugs.

-      {data.map((todo, i) => (
-        <li key={i}>{todo.title}</li>
+      {data.map((todo) => (
+        <li key={todo.id}>{todo.title}</li>
       ))}

If your store doesn’t expose id, consider adding it or using another stable key.


113-113: Tighten phrasing per style lint

Minor wording improvement for a more concise tone.

-Contributions are welcome! Please feel free to submit a Pull Request.
+Contributions welcome. Please open a Pull Request.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2b19260 and 4fbe9d6.

📒 Files selected for processing (2)
  • packages/react-kitchen-sink/README.md (1 hunks)
  • packages/rxjs-firebase/README.md (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/rxjs-firebase/README.md
🧰 Additional context used
🪛 LanguageTool
packages/react-kitchen-sink/README.md

[style] ~113-~113: Consider using a less common alternative to make your writing sound more unique and professional.
Context: ...ontributing Contributions are welcome! Please feel free to submit a Pull Request. ## Support If ...

(FEEL_FREE_TO_STYLE_ME)

Comment on lines +75 to +78
import { useStore } from 'zustand'
import { useSchemaDocumentStore, useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'
import { createFirestoreFactory, collectionSchema } from 'zod-firebase'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix imports in the Zustand example

  • Remove unused useSchemaDocumentStore.
  • Replace unused createFirestoreFactory, collectionSchema with collectionsBuilder (matches usage).
  • Add missing z import.

This makes the snippet copy/paste-ready.

 import { useStore } from 'zustand'
-import { useSchemaDocumentStore, useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'
-import { createFirestoreFactory, collectionSchema } from 'zod-firebase'
+import { useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'
+import { collectionsBuilder } from 'zod-firebase'
+import { z } from 'zod'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useStore } from 'zustand'
import { useSchemaDocumentStore, useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'
import { createFirestoreFactory, collectionSchema } from 'zod-firebase'
import { useStore } from 'zustand'
import { useSchemaQueryStore } from '@valian/react-kitchen-sink/hooks'
import { collectionsBuilder } from 'zod-firebase'
import { z } from 'zod'
🤖 Prompt for AI Agents
In packages/react-kitchen-sink/README.md around lines 75 to 78, the Zustand
example imports include unused and incorrect symbols; update the import line to
remove useSchemaDocumentStore, replace createFirestoreFactory and
collectionSchema with collectionsBuilder, and add an import for z from 'zod' so
the snippet matches usage and is copy/paste-ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant