@neetru/sdk 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +554 -530
  2. package/dist/auth.cjs +60 -39
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.d.cts +1 -2
  5. package/dist/auth.d.ts +1 -2
  6. package/dist/auth.mjs +60 -39
  7. package/dist/auth.mjs.map +1 -1
  8. package/dist/catalog.d.cts +1 -2
  9. package/dist/catalog.d.ts +1 -2
  10. package/dist/checkout.d.cts +1 -2
  11. package/dist/checkout.d.ts +1 -2
  12. package/dist/db-react.cjs +15 -5
  13. package/dist/db-react.cjs.map +1 -1
  14. package/dist/db-react.d.cts +35 -13
  15. package/dist/db-react.d.ts +35 -13
  16. package/dist/db-react.mjs +15 -5
  17. package/dist/db-react.mjs.map +1 -1
  18. package/dist/db.cjs +71 -50
  19. package/dist/db.cjs.map +1 -1
  20. package/dist/db.d.cts +1 -2
  21. package/dist/db.d.ts +1 -2
  22. package/dist/db.mjs +71 -50
  23. package/dist/db.mjs.map +1 -1
  24. package/dist/entitlements.d.cts +1 -2
  25. package/dist/entitlements.d.ts +1 -2
  26. package/dist/firestore-compat.cjs +399 -0
  27. package/dist/firestore-compat.cjs.map +1 -0
  28. package/dist/firestore-compat.d.cts +294 -0
  29. package/dist/firestore-compat.d.ts +294 -0
  30. package/dist/firestore-compat.mjs +371 -0
  31. package/dist/firestore-compat.mjs.map +1 -0
  32. package/dist/index.cjs +72 -51
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +2 -3
  35. package/dist/index.d.ts +2 -3
  36. package/dist/index.mjs +72 -51
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/mocks.d.cts +1 -2
  39. package/dist/mocks.d.ts +1 -2
  40. package/dist/notifications.d.cts +1 -2
  41. package/dist/notifications.d.ts +1 -2
  42. package/dist/react.cjs +267 -0
  43. package/dist/react.cjs.map +1 -1
  44. package/dist/react.d.cts +117 -3
  45. package/dist/react.d.ts +117 -3
  46. package/dist/react.mjs +265 -1
  47. package/dist/react.mjs.map +1 -1
  48. package/dist/support.d.cts +1 -2
  49. package/dist/support.d.ts +1 -2
  50. package/dist/telemetry.d.cts +1 -2
  51. package/dist/telemetry.d.ts +1 -2
  52. package/dist/{types-D7zVkO3n.d.ts → types-BRv8wBxX.d.ts} +581 -2
  53. package/dist/{types-Dc4bjrrt.d.cts → types-nwErcRX8.d.cts} +581 -2
  54. package/dist/usage.d.cts +1 -2
  55. package/dist/usage.d.ts +1 -2
  56. package/dist/webhooks.d.cts +1 -2
  57. package/dist/webhooks.d.ts +1 -2
  58. package/package.json +156 -151
  59. package/dist/collection-ref-BDdfD87k.d.cts +0 -581
  60. package/dist/collection-ref-BDdfD87k.d.ts +0 -581
@@ -1,5 +1,4 @@
1
- import { I as ResolvedConfig, C as CatalogListOptions, b as CatalogListResponse, P as Product } from './types-Dc4bjrrt.cjs';
2
- import './collection-ref-BDdfD87k.cjs';
1
+ import { a1 as ResolvedConfig, C as CatalogListOptions, b as CatalogListResponse, W as Product } from './types-nwErcRX8.cjs';
3
2
  import 'drizzle-orm/node-postgres';
4
3
  import './errors.cjs';
5
4
 
package/dist/catalog.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { I as ResolvedConfig, C as CatalogListOptions, b as CatalogListResponse, P as Product } from './types-D7zVkO3n.js';
2
- import './collection-ref-BDdfD87k.js';
1
+ import { a1 as ResolvedConfig, C as CatalogListOptions, b as CatalogListResponse, W as Product } from './types-BRv8wBxX.js';
3
2
  import 'drizzle-orm/node-postgres';
4
3
  import './errors.js';
5
4
 
@@ -1,4 +1,3 @@
1
- export { c as CheckoutIntentInfo, d as CheckoutIntentStatus, e as CheckoutNamespace, f as CheckoutStartInput, g as CheckoutStartResult, h as CheckoutTenantType, M as MockCheckout, a6 as createCheckoutNamespace } from './types-Dc4bjrrt.cjs';
2
- import './collection-ref-BDdfD87k.cjs';
1
+ export { c as CheckoutIntentInfo, d as CheckoutIntentStatus, e as CheckoutNamespace, f as CheckoutStartInput, g as CheckoutStartResult, h as CheckoutTenantType, M as MockCheckout, aq as createCheckoutNamespace } from './types-nwErcRX8.cjs';
3
2
  import 'drizzle-orm/node-postgres';
4
3
  import './errors.cjs';
@@ -1,4 +1,3 @@
1
- export { c as CheckoutIntentInfo, d as CheckoutIntentStatus, e as CheckoutNamespace, f as CheckoutStartInput, g as CheckoutStartResult, h as CheckoutTenantType, M as MockCheckout, a6 as createCheckoutNamespace } from './types-D7zVkO3n.js';
2
- import './collection-ref-BDdfD87k.js';
1
+ export { c as CheckoutIntentInfo, d as CheckoutIntentStatus, e as CheckoutNamespace, f as CheckoutStartInput, g as CheckoutStartResult, h as CheckoutTenantType, M as MockCheckout, aq as createCheckoutNamespace } from './types-BRv8wBxX.js';
3
2
  import 'drizzle-orm/node-postgres';
4
3
  import './errors.js';
package/dist/db-react.cjs CHANGED
@@ -23,6 +23,15 @@ function _interopNamespace(e) {
23
23
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
24
24
 
25
25
  // src/db/react.ts
26
+ function isNeetruClient(input) {
27
+ const maybe = input;
28
+ if (typeof maybe.syncState !== "undefined") return false;
29
+ const db = maybe.db;
30
+ return !!db && typeof db.syncState !== "undefined" && typeof db.onSyncStateChanged === "function";
31
+ }
32
+ function resolveSyncStateSource(input) {
33
+ return isNeetruClient(input) ? input.db : input;
34
+ }
26
35
  function serializeQuery(query) {
27
36
  if (!query) return "";
28
37
  try {
@@ -109,15 +118,16 @@ function useCollection(ref, query) {
109
118
  }, [ref, queryKey]);
110
119
  return state;
111
120
  }
112
- function useSyncState(ns) {
113
- const [state, setState] = React__namespace.useState(() => ns.syncState);
121
+ function useSyncState(input) {
122
+ const source = resolveSyncStateSource(input);
123
+ const [state, setState] = React__namespace.useState(() => source.syncState);
114
124
  const mountedRef = React__namespace.useRef(true);
115
125
  React__namespace.useEffect(() => {
116
126
  mountedRef.current = true;
117
127
  if (mountedRef.current) {
118
- setState(ns.syncState);
128
+ setState(source.syncState);
119
129
  }
120
- const unsubscribe = ns.onSyncStateChanged((s) => {
130
+ const unsubscribe = source.onSyncStateChanged((s) => {
121
131
  if (!mountedRef.current) return;
122
132
  setState(s);
123
133
  });
@@ -125,7 +135,7 @@ function useSyncState(ns) {
125
135
  mountedRef.current = false;
126
136
  unsubscribe();
127
137
  };
128
- }, [ns]);
138
+ }, [source]);
129
139
  return state;
130
140
  }
131
141
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/db/react.ts"],"names":["React"],"mappings":"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","file":"db-react.cjs","sourcesContent":["/**\r\n * Bindings React do módulo db (M2) — `@neetru/sdk/react`.\r\n *\r\n * Exporta três hooks de tempo real que se integram com a camada offline do\r\n * `@neetru/sdk/db`:\r\n *\r\n * - `useDocument(ref, id)` — subscreve ao onSnapshot de um documento.\r\n * - `useCollection(ref, query?)` — subscreve ao onSnapshot de uma coleção.\r\n * - `useSyncState(ns)` — observa o SyncState do namespace offline.\r\n *\r\n * ### Design\r\n * Segue o padrão do subpath `/react` existente (cliente/ref passados\r\n * explicitamente, sem Context Provider obrigatório). Alinhado com 02-sdk.md §3.6.\r\n *\r\n * ### React 18/19 StrictMode\r\n * Cada hook usa um ref interno para rastrear se o componente foi desmontado,\r\n * e a função `useEffect` retorna sempre a função de unsubscribe. Em StrictMode,\r\n * o duplo-mount/unmount do React 18 chama cleanup corretamente — não há leak.\r\n *\r\n * ### Dependências\r\n * `react` é peer dependency — não importada como dep direta.\r\n */\r\nimport * as React from 'react';\r\nimport type { DbCollectionRef, DbDoc, DbQuery, DbListResult, DbGetResult } from './collection-ref';\r\nimport type { SyncState, Unsubscribe } from './offline/types';\r\n\r\n// ─── Tipos públicos dos hooks ─────────────────────────────────────────────────\r\n\r\n/**\r\n * Resultado de `useDocument`.\r\n *\r\n * - `data` — dados do documento, ou `null` se não existe / ainda carregando.\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local, não do servidor.\r\n * - `stale` — conexão caída ou sincronizando; pode haver dados mais novos.\r\n */\r\nexport interface UseDocumentResult<T> {\r\n data: T | null;\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n}\r\n\r\n/**\r\n * Resultado de `useCollection`.\r\n *\r\n * - `docs` — documentos da coleção (ou subconjunto via query).\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local.\r\n * - `stale` — conexão caída ou sincronizando.\r\n * - `hasPendingWrites` — ao menos um doc tem escrita local não confirmada.\r\n */\r\nexport interface UseCollectionResult<T> {\r\n docs: DbDoc<T>[];\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n hasPendingWrites: boolean;\r\n}\r\n\r\n/**\r\n * Subconjunto mínimo do namespace offline que `useSyncState` precisa.\r\n * Compatível com `NeetruDbDocuments` de `collection-ref.ts`.\r\n *\r\n * Passado explicitamente (sem Context) para alinhar com o padrão do SDK.\r\n * Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.\r\n */\r\nexport interface SyncStateSource {\r\n readonly syncState: SyncState;\r\n onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;\r\n}\r\n\r\n// ─── Serialização estável de query ────────────────────────────────────────────\r\n\r\n/**\r\n * Serializa uma DbQuery para string estável, usada como chave de efeito.\r\n *\r\n * `useCollection` re-subscreve somente quando a serialização muda, NÃO por\r\n * identidade de objeto. Isso evita a armadilha clássica de inline-object em\r\n * JSX que causaria re-subscribe em todo render.\r\n */\r\nfunction serializeQuery(query: DbQuery | undefined): string {\r\n if (!query) return '';\r\n try {\r\n return JSON.stringify(query, Object.keys(query).sort());\r\n } catch {\r\n return String(query);\r\n }\r\n}\r\n\r\n// ─── useDocument ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para um documento específico.\r\n *\r\n * Subscreve a `ref.doc(id).onSnapshot` no mount. Re-subscreve quando `ref`\r\n * ou `id` mudam. Cancela a subscrição no unmount.\r\n *\r\n * ```tsx\r\n * const { data, loading, fromCache, stale } = useDocument(ordersRef, orderId);\r\n * if (loading) return <Spinner />;\r\n * if (!data) return <NotFound />;\r\n * return <OrderCard order={data} fromCache={fromCache} />;\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode: a cleanup function do useEffect cancela\r\n * o listener no unmount, e o double-mount do StrictMode não vaza estado.\r\n */\r\nexport function useDocument<T>(\r\n ref: DbCollectionRef<T>,\r\n id: string,\r\n): UseDocumentResult<T> {\r\n const [state, setState] = React.useState<UseDocumentResult<T>>({\r\n data: null,\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n });\r\n\r\n // Ref para detectar unmount e evitar setState após cleanup.\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset para loading quando ref/id muda\r\n setState({ data: null, loading: true, fromCache: true, stale: false });\r\n\r\n const docRef = ref.doc(id);\r\n\r\n const unsubscribe: Unsubscribe = docRef.onSnapshot(\r\n (snap: DbGetResult<T> | null) => {\r\n if (!mountedRef.current) return;\r\n if (snap === null || snap.docs.length === 0) {\r\n setState({\r\n data: null,\r\n loading: false,\r\n fromCache: snap?.fromCache ?? true,\r\n stale: snap?.stale ?? false,\r\n });\r\n } else {\r\n setState({\r\n data: snap.docs[0]?.data ?? null,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n });\r\n }\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, id]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useCollection ────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para uma coleção (ou subconjunto filtrado).\r\n *\r\n * Subscreve a `ref.onSnapshot(query, cb)` no mount. Re-subscreve quando `ref`\r\n * ou a serialização de `query` muda — NÃO por identidade de objeto, o que\r\n * evita re-subscribe desnecessário em inline objects.\r\n *\r\n * ```tsx\r\n * const { docs, loading, hasPendingWrites } = useCollection(ordersRef, {\r\n * where: [{ field: 'status', op: '==', value: 'open' }],\r\n * });\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useCollection<T>(\r\n ref: DbCollectionRef<T>,\r\n query?: DbQuery,\r\n): UseCollectionResult<T> {\r\n const [state, setState] = React.useState<UseCollectionResult<T>>({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n // Serializamos a query para garantir que mudanças de valor (não de referência)\r\n // triggam o re-subscribe, mas que objetos iguais não causem loop.\r\n const queryKey = serializeQuery(query);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset loading ao trocar de query/ref\r\n setState({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const unsubscribe: Unsubscribe = ref.onSnapshot(\r\n query,\r\n (snap: DbListResult<T>) => {\r\n if (!mountedRef.current) return;\r\n setState({\r\n docs: snap.docs,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n hasPendingWrites: snap.hasPendingWrites,\r\n });\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, queryKey]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useSyncState ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook para observar o SyncState do namespace offline.\r\n *\r\n * Retorna o `SyncState` atual e re-renderiza quando ele muda.\r\n * Usado para renderizar indicadores \"sincronizando…\" ou \"offline\" na UI.\r\n *\r\n * ```tsx\r\n * const state = useSyncState(ns);\r\n * if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;\r\n * ```\r\n *\r\n * `ns` deve ser o namespace retornado por `createOfflineDocumentsNamespace` (ou\r\n * qualquer objeto com `syncState` + `onSyncStateChanged`).\r\n *\r\n * Alinhado com 02-sdk.md §3.6: `useSyncState(client: NeetruClient): NeetruSyncState`.\r\n * Na camada M2, aceita diretamente o namespace de documentos (`SyncStateSource`)\r\n * pois o `NeetruClient.db` completo está fora do escopo do M2.\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useSyncState(ns: SyncStateSource): SyncState {\r\n const [state, setState] = React.useState<SyncState>(() => ns.syncState);\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n\r\n // Snapshot inicial imediato ao montar (pode ter mudado entre render e effect)\r\n if (mountedRef.current) {\r\n setState(ns.syncState);\r\n }\r\n\r\n const unsubscribe: Unsubscribe = ns.onSyncStateChanged((s: SyncState) => {\r\n if (!mountedRef.current) return;\r\n setState(s);\r\n });\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ns]);\r\n\r\n return state;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/db/react.ts"],"names":["React"],"mappings":"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","file":"db-react.cjs","sourcesContent":["/**\r\n * Bindings React do módulo db (M2) — `@neetru/sdk/react`.\r\n *\r\n * Exporta três hooks de tempo real que se integram com a camada offline do\r\n * `@neetru/sdk/db`:\r\n *\r\n * - `useDocument(ref, id)` — subscreve ao onSnapshot de um documento.\r\n * - `useCollection(ref, query?)` — subscreve ao onSnapshot de uma coleção.\r\n * - `useSyncState(ns)` — observa o SyncState do namespace offline.\r\n *\r\n * ### Design\r\n * Segue o padrão do subpath `/react` existente (cliente/ref passados\r\n * explicitamente, sem Context Provider obrigatório). Alinhado com 02-sdk.md §3.6.\r\n *\r\n * ### React 18/19 StrictMode\r\n * Cada hook usa um ref interno para rastrear se o componente foi desmontado,\r\n * e a função `useEffect` retorna sempre a função de unsubscribe. Em StrictMode,\r\n * o duplo-mount/unmount do React 18 chama cleanup corretamente — não há leak.\r\n *\r\n * ### Dependências\r\n * `react` é peer dependency — não importada como dep direta.\r\n */\r\nimport * as React from 'react';\r\nimport type { DbCollectionRef, DbDoc, DbQuery, DbListResult, DbGetResult } from './collection-ref';\r\nimport type { SyncState, Unsubscribe } from './offline/types';\r\nimport type { NeetruClient } from '../types';\r\n\r\n// ─── Tipos públicos dos hooks ─────────────────────────────────────────────────\r\n\r\n/**\r\n * Resultado de `useDocument`.\r\n *\r\n * - `data` — dados do documento, ou `null` se não existe / ainda carregando.\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local, não do servidor.\r\n * - `stale` — conexão caída ou sincronizando; pode haver dados mais novos.\r\n */\r\nexport interface UseDocumentResult<T> {\r\n data: T | null;\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n}\r\n\r\n/**\r\n * Resultado de `useCollection`.\r\n *\r\n * - `docs` — documentos da coleção (ou subconjunto via query).\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local.\r\n * - `stale` — conexão caída ou sincronizando.\r\n * - `hasPendingWrites` — ao menos um doc tem escrita local não confirmada.\r\n */\r\nexport interface UseCollectionResult<T> {\r\n docs: DbDoc<T>[];\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n hasPendingWrites: boolean;\r\n}\r\n\r\n/**\r\n * Subconjunto mínimo do namespace offline que `useSyncState` precisa.\r\n * Compatível com `NeetruDbDocuments` de `collection-ref.ts`.\r\n *\r\n * Passado explicitamente (sem Context) para alinhar com o padrão do SDK.\r\n * Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.\r\n *\r\n * `client.db` (o `NeetruDb` retornado por `createNeetruClient`) também satisfaz\r\n * este contrato — expõe `syncState` + `onSyncStateChanged` reais. Por isso\r\n * `useSyncState` aceita tanto a fonte direta quanto o `NeetruClient` inteiro\r\n * (ver `UseSyncStateInput`).\r\n */\r\nexport interface SyncStateSource {\r\n readonly syncState: SyncState;\r\n onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;\r\n}\r\n\r\n/**\r\n * Entrada aceita por `useSyncState`:\r\n * - `NeetruClient` → resolve `client.db` automaticamente (forma oficial,\r\n * alinhada com 02-sdk.md §3.6 `useSyncState(client)`).\r\n * - `SyncStateSource` → fonte direta (`client.db` ou o namespace offline cru).\r\n */\r\nexport type UseSyncStateInput = NeetruClient | SyncStateSource;\r\n\r\n/**\r\n * Type guard — detecta se a entrada é um `NeetruClient` (tem `.db` que é, ele\r\n * próprio, um `SyncStateSource`) e não uma fonte direta. Distinção em runtime\r\n * porque os shapes não se sobrepõem: a fonte direta expõe `syncState` no nível\r\n * de topo; o client expõe via `.db`.\r\n */\r\nfunction isNeetruClient(input: UseSyncStateInput): input is NeetruClient {\r\n const maybe = input as { db?: unknown; syncState?: unknown };\r\n // Fonte direta já tem `syncState` no topo → não é o client wrapper.\r\n if (typeof maybe.syncState !== 'undefined') return false;\r\n const db = maybe.db as { syncState?: unknown; onSyncStateChanged?: unknown } | undefined;\r\n return (\r\n !!db &&\r\n typeof db.syncState !== 'undefined' &&\r\n typeof db.onSyncStateChanged === 'function'\r\n );\r\n}\r\n\r\n/** Normaliza qualquer entrada aceita para a `SyncStateSource` subjacente. */\r\nfunction resolveSyncStateSource(input: UseSyncStateInput): SyncStateSource {\r\n return isNeetruClient(input) ? (input.db as unknown as SyncStateSource) : input;\r\n}\r\n\r\n// ─── Serialização estável de query ────────────────────────────────────────────\r\n\r\n/**\r\n * Serializa uma DbQuery para string estável, usada como chave de efeito.\r\n *\r\n * `useCollection` re-subscreve somente quando a serialização muda, NÃO por\r\n * identidade de objeto. Isso evita a armadilha clássica de inline-object em\r\n * JSX que causaria re-subscribe em todo render.\r\n */\r\nfunction serializeQuery(query: DbQuery | undefined): string {\r\n if (!query) return '';\r\n try {\r\n return JSON.stringify(query, Object.keys(query).sort());\r\n } catch {\r\n return String(query);\r\n }\r\n}\r\n\r\n// ─── useDocument ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para um documento específico.\r\n *\r\n * Subscreve a `ref.doc(id).onSnapshot` no mount. Re-subscreve quando `ref`\r\n * ou `id` mudam. Cancela a subscrição no unmount.\r\n *\r\n * ```tsx\r\n * const { data, loading, fromCache, stale } = useDocument(ordersRef, orderId);\r\n * if (loading) return <Spinner />;\r\n * if (!data) return <NotFound />;\r\n * return <OrderCard order={data} fromCache={fromCache} />;\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode: a cleanup function do useEffect cancela\r\n * o listener no unmount, e o double-mount do StrictMode não vaza estado.\r\n */\r\nexport function useDocument<T>(\r\n ref: DbCollectionRef<T>,\r\n id: string,\r\n): UseDocumentResult<T> {\r\n const [state, setState] = React.useState<UseDocumentResult<T>>({\r\n data: null,\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n });\r\n\r\n // Ref para detectar unmount e evitar setState após cleanup.\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset para loading quando ref/id muda\r\n setState({ data: null, loading: true, fromCache: true, stale: false });\r\n\r\n const docRef = ref.doc(id);\r\n\r\n const unsubscribe: Unsubscribe = docRef.onSnapshot(\r\n (snap: DbGetResult<T> | null) => {\r\n if (!mountedRef.current) return;\r\n if (snap === null || snap.docs.length === 0) {\r\n setState({\r\n data: null,\r\n loading: false,\r\n fromCache: snap?.fromCache ?? true,\r\n stale: snap?.stale ?? false,\r\n });\r\n } else {\r\n setState({\r\n data: snap.docs[0]?.data ?? null,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n });\r\n }\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, id]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useCollection ────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para uma coleção (ou subconjunto filtrado).\r\n *\r\n * Subscreve a `ref.onSnapshot(query, cb)` no mount. Re-subscreve quando `ref`\r\n * ou a serialização de `query` muda — NÃO por identidade de objeto, o que\r\n * evita re-subscribe desnecessário em inline objects.\r\n *\r\n * ```tsx\r\n * const { docs, loading, hasPendingWrites } = useCollection(ordersRef, {\r\n * where: [{ field: 'status', op: '==', value: 'open' }],\r\n * });\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useCollection<T>(\r\n ref: DbCollectionRef<T>,\r\n query?: DbQuery,\r\n): UseCollectionResult<T> {\r\n const [state, setState] = React.useState<UseCollectionResult<T>>({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n // Serializamos a query para garantir que mudanças de valor (não de referência)\r\n // triggam o re-subscribe, mas que objetos iguais não causem loop.\r\n const queryKey = serializeQuery(query);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset loading ao trocar de query/ref\r\n setState({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const unsubscribe: Unsubscribe = ref.onSnapshot(\r\n query,\r\n (snap: DbListResult<T>) => {\r\n if (!mountedRef.current) return;\r\n setState({\r\n docs: snap.docs,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n hasPendingWrites: snap.hasPendingWrites,\r\n });\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, queryKey]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useSyncState ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook para observar o `SyncState` da camada offline em tempo real.\r\n *\r\n * Retorna o `SyncState` atual e re-renderiza quando ele muda. Usado para\r\n * renderizar indicadores \"sincronizando…\" / \"offline\" / contadores de escritas\r\n * pendentes na UI.\r\n *\r\n * Aceita duas formas de entrada (SDK-QW-4 — wiring ao client real):\r\n *\r\n * ```tsx\r\n * // Forma oficial — passe o client; o hook resolve client.db internamente.\r\n * const client = createNeetruClient({ apiKey });\r\n * const state = useSyncState(client);\r\n *\r\n * // Forma direta — passe a fonte (client.db ou o namespace offline cru).\r\n * const state = useSyncState(client.db);\r\n *\r\n * if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;\r\n * ```\r\n *\r\n * Antes desta versão o hook só aceitava a `SyncStateSource` crua, o que levava\r\n * consumers a stub-arem o estado pra sempre `'idle'` em vez de ligar no client.\r\n * Agora `useSyncState(client)` (a forma documentada em 02-sdk.md §3.6) funciona\r\n * de ponta a ponta — `client.db.syncState` / `client.db.onSyncStateChanged` são\r\n * reais.\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useSyncState(input: UseSyncStateInput): SyncState {\r\n // Resolve uma única vez por render; se a identidade da fonte resolvida mudar,\r\n // o efeito abaixo re-subscreve (dependência `source`).\r\n const source = resolveSyncStateSource(input);\r\n\r\n const [state, setState] = React.useState<SyncState>(() => source.syncState);\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n\r\n // Snapshot inicial imediato ao montar (pode ter mudado entre render e effect)\r\n if (mountedRef.current) {\r\n setState(source.syncState);\r\n }\r\n\r\n const unsubscribe: Unsubscribe = source.onSyncStateChanged((s: SyncState) => {\r\n if (!mountedRef.current) return;\r\n setState(s);\r\n });\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [source]);\r\n\r\n return state;\r\n}\r\n"]}
@@ -1,4 +1,6 @@
1
- import { k as SyncState, U as Unsubscribe, d as DbDoc, c as DbCollectionRef, h as DbQuery } from './collection-ref-BDdfD87k.cjs';
1
+ import { aa as SyncState, af as Unsubscribe, o as DbDoc, z as NeetruClient, n as DbCollectionRef, t as DbQuery } from './types-nwErcRX8.cjs';
2
+ import 'drizzle-orm/node-postgres';
3
+ import './errors.cjs';
2
4
 
3
5
  /**
4
6
  * Resultado de `useDocument`.
@@ -36,11 +38,23 @@ interface UseCollectionResult<T> {
36
38
  *
37
39
  * Passado explicitamente (sem Context) para alinhar com o padrão do SDK.
38
40
  * Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.
41
+ *
42
+ * `client.db` (o `NeetruDb` retornado por `createNeetruClient`) também satisfaz
43
+ * este contrato — expõe `syncState` + `onSyncStateChanged` reais. Por isso
44
+ * `useSyncState` aceita tanto a fonte direta quanto o `NeetruClient` inteiro
45
+ * (ver `UseSyncStateInput`).
39
46
  */
40
47
  interface SyncStateSource {
41
48
  readonly syncState: SyncState;
42
49
  onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;
43
50
  }
51
+ /**
52
+ * Entrada aceita por `useSyncState`:
53
+ * - `NeetruClient` → resolve `client.db` automaticamente (forma oficial,
54
+ * alinhada com 02-sdk.md §3.6 `useSyncState(client)`).
55
+ * - `SyncStateSource` → fonte direta (`client.db` ou o namespace offline cru).
56
+ */
57
+ type UseSyncStateInput = NeetruClient | SyncStateSource;
44
58
  /**
45
59
  * Hook de tempo real para um documento específico.
46
60
  *
@@ -75,25 +89,33 @@ declare function useDocument<T>(ref: DbCollectionRef<T>, id: string): UseDocumen
75
89
  */
76
90
  declare function useCollection<T>(ref: DbCollectionRef<T>, query?: DbQuery): UseCollectionResult<T>;
77
91
  /**
78
- * Hook para observar o SyncState do namespace offline.
92
+ * Hook para observar o `SyncState` da camada offline em tempo real.
93
+ *
94
+ * Retorna o `SyncState` atual e re-renderiza quando ele muda. Usado para
95
+ * renderizar indicadores "sincronizando…" / "offline" / contadores de escritas
96
+ * pendentes na UI.
79
97
  *
80
- * Retorna o `SyncState` atual e re-renderiza quando ele muda.
81
- * Usado para renderizar indicadores "sincronizando…" ou "offline" na UI.
98
+ * Aceita duas formas de entrada (SDK-QW-4 wiring ao client real):
82
99
  *
83
100
  * ```tsx
84
- * const state = useSyncState(ns);
101
+ * // Forma oficial — passe o client; o hook resolve client.db internamente.
102
+ * const client = createNeetruClient({ apiKey });
103
+ * const state = useSyncState(client);
104
+ *
105
+ * // Forma direta — passe a fonte (client.db ou o namespace offline cru).
106
+ * const state = useSyncState(client.db);
107
+ *
85
108
  * if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;
86
109
  * ```
87
110
  *
88
- * `ns` deve ser o namespace retornado por `createOfflineDocumentsNamespace` (ou
89
- * qualquer objeto com `syncState` + `onSyncStateChanged`).
90
- *
91
- * Alinhado com 02-sdk.md §3.6: `useSyncState(client: NeetruClient): NeetruSyncState`.
92
- * Na camada M2, aceita diretamente o namespace de documentos (`SyncStateSource`)
93
- * pois o `NeetruClient.db` completo está fora do escopo do M2.
111
+ * Antes desta versão o hook aceitava a `SyncStateSource` crua, o que levava
112
+ * consumers a stub-arem o estado pra sempre `'idle'` em vez de ligar no client.
113
+ * Agora `useSyncState(client)` (a forma documentada em 02-sdk.md §3.6) funciona
114
+ * de ponta a ponta — `client.db.syncState` / `client.db.onSyncStateChanged` são
115
+ * reais.
94
116
  *
95
117
  * Seguro em React 18/19 StrictMode.
96
118
  */
97
- declare function useSyncState(ns: SyncStateSource): SyncState;
119
+ declare function useSyncState(input: UseSyncStateInput): SyncState;
98
120
 
99
- export { type SyncStateSource, type UseCollectionResult, type UseDocumentResult, useCollection, useDocument, useSyncState };
121
+ export { type SyncStateSource, type UseCollectionResult, type UseDocumentResult, type UseSyncStateInput, useCollection, useDocument, useSyncState };
@@ -1,4 +1,6 @@
1
- import { k as SyncState, U as Unsubscribe, d as DbDoc, c as DbCollectionRef, h as DbQuery } from './collection-ref-BDdfD87k.js';
1
+ import { aa as SyncState, af as Unsubscribe, o as DbDoc, z as NeetruClient, n as DbCollectionRef, t as DbQuery } from './types-BRv8wBxX.js';
2
+ import 'drizzle-orm/node-postgres';
3
+ import './errors.js';
2
4
 
3
5
  /**
4
6
  * Resultado de `useDocument`.
@@ -36,11 +38,23 @@ interface UseCollectionResult<T> {
36
38
  *
37
39
  * Passado explicitamente (sem Context) para alinhar com o padrão do SDK.
38
40
  * Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.
41
+ *
42
+ * `client.db` (o `NeetruDb` retornado por `createNeetruClient`) também satisfaz
43
+ * este contrato — expõe `syncState` + `onSyncStateChanged` reais. Por isso
44
+ * `useSyncState` aceita tanto a fonte direta quanto o `NeetruClient` inteiro
45
+ * (ver `UseSyncStateInput`).
39
46
  */
40
47
  interface SyncStateSource {
41
48
  readonly syncState: SyncState;
42
49
  onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;
43
50
  }
51
+ /**
52
+ * Entrada aceita por `useSyncState`:
53
+ * - `NeetruClient` → resolve `client.db` automaticamente (forma oficial,
54
+ * alinhada com 02-sdk.md §3.6 `useSyncState(client)`).
55
+ * - `SyncStateSource` → fonte direta (`client.db` ou o namespace offline cru).
56
+ */
57
+ type UseSyncStateInput = NeetruClient | SyncStateSource;
44
58
  /**
45
59
  * Hook de tempo real para um documento específico.
46
60
  *
@@ -75,25 +89,33 @@ declare function useDocument<T>(ref: DbCollectionRef<T>, id: string): UseDocumen
75
89
  */
76
90
  declare function useCollection<T>(ref: DbCollectionRef<T>, query?: DbQuery): UseCollectionResult<T>;
77
91
  /**
78
- * Hook para observar o SyncState do namespace offline.
92
+ * Hook para observar o `SyncState` da camada offline em tempo real.
93
+ *
94
+ * Retorna o `SyncState` atual e re-renderiza quando ele muda. Usado para
95
+ * renderizar indicadores "sincronizando…" / "offline" / contadores de escritas
96
+ * pendentes na UI.
79
97
  *
80
- * Retorna o `SyncState` atual e re-renderiza quando ele muda.
81
- * Usado para renderizar indicadores "sincronizando…" ou "offline" na UI.
98
+ * Aceita duas formas de entrada (SDK-QW-4 wiring ao client real):
82
99
  *
83
100
  * ```tsx
84
- * const state = useSyncState(ns);
101
+ * // Forma oficial — passe o client; o hook resolve client.db internamente.
102
+ * const client = createNeetruClient({ apiKey });
103
+ * const state = useSyncState(client);
104
+ *
105
+ * // Forma direta — passe a fonte (client.db ou o namespace offline cru).
106
+ * const state = useSyncState(client.db);
107
+ *
85
108
  * if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;
86
109
  * ```
87
110
  *
88
- * `ns` deve ser o namespace retornado por `createOfflineDocumentsNamespace` (ou
89
- * qualquer objeto com `syncState` + `onSyncStateChanged`).
90
- *
91
- * Alinhado com 02-sdk.md §3.6: `useSyncState(client: NeetruClient): NeetruSyncState`.
92
- * Na camada M2, aceita diretamente o namespace de documentos (`SyncStateSource`)
93
- * pois o `NeetruClient.db` completo está fora do escopo do M2.
111
+ * Antes desta versão o hook aceitava a `SyncStateSource` crua, o que levava
112
+ * consumers a stub-arem o estado pra sempre `'idle'` em vez de ligar no client.
113
+ * Agora `useSyncState(client)` (a forma documentada em 02-sdk.md §3.6) funciona
114
+ * de ponta a ponta — `client.db.syncState` / `client.db.onSyncStateChanged` são
115
+ * reais.
94
116
  *
95
117
  * Seguro em React 18/19 StrictMode.
96
118
  */
97
- declare function useSyncState(ns: SyncStateSource): SyncState;
119
+ declare function useSyncState(input: UseSyncStateInput): SyncState;
98
120
 
99
- export { type SyncStateSource, type UseCollectionResult, type UseDocumentResult, useCollection, useDocument, useSyncState };
121
+ export { type SyncStateSource, type UseCollectionResult, type UseDocumentResult, type UseSyncStateInput, useCollection, useDocument, useSyncState };
package/dist/db-react.mjs CHANGED
@@ -1,6 +1,15 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  // src/db/react.ts
4
+ function isNeetruClient(input) {
5
+ const maybe = input;
6
+ if (typeof maybe.syncState !== "undefined") return false;
7
+ const db = maybe.db;
8
+ return !!db && typeof db.syncState !== "undefined" && typeof db.onSyncStateChanged === "function";
9
+ }
10
+ function resolveSyncStateSource(input) {
11
+ return isNeetruClient(input) ? input.db : input;
12
+ }
4
13
  function serializeQuery(query) {
5
14
  if (!query) return "";
6
15
  try {
@@ -87,15 +96,16 @@ function useCollection(ref, query) {
87
96
  }, [ref, queryKey]);
88
97
  return state;
89
98
  }
90
- function useSyncState(ns) {
91
- const [state, setState] = React.useState(() => ns.syncState);
99
+ function useSyncState(input) {
100
+ const source = resolveSyncStateSource(input);
101
+ const [state, setState] = React.useState(() => source.syncState);
92
102
  const mountedRef = React.useRef(true);
93
103
  React.useEffect(() => {
94
104
  mountedRef.current = true;
95
105
  if (mountedRef.current) {
96
- setState(ns.syncState);
106
+ setState(source.syncState);
97
107
  }
98
- const unsubscribe = ns.onSyncStateChanged((s) => {
108
+ const unsubscribe = source.onSyncStateChanged((s) => {
99
109
  if (!mountedRef.current) return;
100
110
  setState(s);
101
111
  });
@@ -103,7 +113,7 @@ function useSyncState(ns) {
103
113
  mountedRef.current = false;
104
114
  unsubscribe();
105
115
  };
106
- }, [ns]);
116
+ }, [source]);
107
117
  return state;
108
118
  }
109
119
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/db/react.ts"],"names":[],"mappings":"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","file":"db-react.mjs","sourcesContent":["/**\r\n * Bindings React do módulo db (M2) — `@neetru/sdk/react`.\r\n *\r\n * Exporta três hooks de tempo real que se integram com a camada offline do\r\n * `@neetru/sdk/db`:\r\n *\r\n * - `useDocument(ref, id)` — subscreve ao onSnapshot de um documento.\r\n * - `useCollection(ref, query?)` — subscreve ao onSnapshot de uma coleção.\r\n * - `useSyncState(ns)` — observa o SyncState do namespace offline.\r\n *\r\n * ### Design\r\n * Segue o padrão do subpath `/react` existente (cliente/ref passados\r\n * explicitamente, sem Context Provider obrigatório). Alinhado com 02-sdk.md §3.6.\r\n *\r\n * ### React 18/19 StrictMode\r\n * Cada hook usa um ref interno para rastrear se o componente foi desmontado,\r\n * e a função `useEffect` retorna sempre a função de unsubscribe. Em StrictMode,\r\n * o duplo-mount/unmount do React 18 chama cleanup corretamente — não há leak.\r\n *\r\n * ### Dependências\r\n * `react` é peer dependency — não importada como dep direta.\r\n */\r\nimport * as React from 'react';\r\nimport type { DbCollectionRef, DbDoc, DbQuery, DbListResult, DbGetResult } from './collection-ref';\r\nimport type { SyncState, Unsubscribe } from './offline/types';\r\n\r\n// ─── Tipos públicos dos hooks ─────────────────────────────────────────────────\r\n\r\n/**\r\n * Resultado de `useDocument`.\r\n *\r\n * - `data` — dados do documento, ou `null` se não existe / ainda carregando.\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local, não do servidor.\r\n * - `stale` — conexão caída ou sincronizando; pode haver dados mais novos.\r\n */\r\nexport interface UseDocumentResult<T> {\r\n data: T | null;\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n}\r\n\r\n/**\r\n * Resultado de `useCollection`.\r\n *\r\n * - `docs` — documentos da coleção (ou subconjunto via query).\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local.\r\n * - `stale` — conexão caída ou sincronizando.\r\n * - `hasPendingWrites` — ao menos um doc tem escrita local não confirmada.\r\n */\r\nexport interface UseCollectionResult<T> {\r\n docs: DbDoc<T>[];\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n hasPendingWrites: boolean;\r\n}\r\n\r\n/**\r\n * Subconjunto mínimo do namespace offline que `useSyncState` precisa.\r\n * Compatível com `NeetruDbDocuments` de `collection-ref.ts`.\r\n *\r\n * Passado explicitamente (sem Context) para alinhar com o padrão do SDK.\r\n * Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.\r\n */\r\nexport interface SyncStateSource {\r\n readonly syncState: SyncState;\r\n onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;\r\n}\r\n\r\n// ─── Serialização estável de query ────────────────────────────────────────────\r\n\r\n/**\r\n * Serializa uma DbQuery para string estável, usada como chave de efeito.\r\n *\r\n * `useCollection` re-subscreve somente quando a serialização muda, NÃO por\r\n * identidade de objeto. Isso evita a armadilha clássica de inline-object em\r\n * JSX que causaria re-subscribe em todo render.\r\n */\r\nfunction serializeQuery(query: DbQuery | undefined): string {\r\n if (!query) return '';\r\n try {\r\n return JSON.stringify(query, Object.keys(query).sort());\r\n } catch {\r\n return String(query);\r\n }\r\n}\r\n\r\n// ─── useDocument ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para um documento específico.\r\n *\r\n * Subscreve a `ref.doc(id).onSnapshot` no mount. Re-subscreve quando `ref`\r\n * ou `id` mudam. Cancela a subscrição no unmount.\r\n *\r\n * ```tsx\r\n * const { data, loading, fromCache, stale } = useDocument(ordersRef, orderId);\r\n * if (loading) return <Spinner />;\r\n * if (!data) return <NotFound />;\r\n * return <OrderCard order={data} fromCache={fromCache} />;\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode: a cleanup function do useEffect cancela\r\n * o listener no unmount, e o double-mount do StrictMode não vaza estado.\r\n */\r\nexport function useDocument<T>(\r\n ref: DbCollectionRef<T>,\r\n id: string,\r\n): UseDocumentResult<T> {\r\n const [state, setState] = React.useState<UseDocumentResult<T>>({\r\n data: null,\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n });\r\n\r\n // Ref para detectar unmount e evitar setState após cleanup.\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset para loading quando ref/id muda\r\n setState({ data: null, loading: true, fromCache: true, stale: false });\r\n\r\n const docRef = ref.doc(id);\r\n\r\n const unsubscribe: Unsubscribe = docRef.onSnapshot(\r\n (snap: DbGetResult<T> | null) => {\r\n if (!mountedRef.current) return;\r\n if (snap === null || snap.docs.length === 0) {\r\n setState({\r\n data: null,\r\n loading: false,\r\n fromCache: snap?.fromCache ?? true,\r\n stale: snap?.stale ?? false,\r\n });\r\n } else {\r\n setState({\r\n data: snap.docs[0]?.data ?? null,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n });\r\n }\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, id]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useCollection ────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para uma coleção (ou subconjunto filtrado).\r\n *\r\n * Subscreve a `ref.onSnapshot(query, cb)` no mount. Re-subscreve quando `ref`\r\n * ou a serialização de `query` muda — NÃO por identidade de objeto, o que\r\n * evita re-subscribe desnecessário em inline objects.\r\n *\r\n * ```tsx\r\n * const { docs, loading, hasPendingWrites } = useCollection(ordersRef, {\r\n * where: [{ field: 'status', op: '==', value: 'open' }],\r\n * });\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useCollection<T>(\r\n ref: DbCollectionRef<T>,\r\n query?: DbQuery,\r\n): UseCollectionResult<T> {\r\n const [state, setState] = React.useState<UseCollectionResult<T>>({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n // Serializamos a query para garantir que mudanças de valor (não de referência)\r\n // triggam o re-subscribe, mas que objetos iguais não causem loop.\r\n const queryKey = serializeQuery(query);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset loading ao trocar de query/ref\r\n setState({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const unsubscribe: Unsubscribe = ref.onSnapshot(\r\n query,\r\n (snap: DbListResult<T>) => {\r\n if (!mountedRef.current) return;\r\n setState({\r\n docs: snap.docs,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n hasPendingWrites: snap.hasPendingWrites,\r\n });\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, queryKey]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useSyncState ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook para observar o SyncState do namespace offline.\r\n *\r\n * Retorna o `SyncState` atual e re-renderiza quando ele muda.\r\n * Usado para renderizar indicadores \"sincronizando…\" ou \"offline\" na UI.\r\n *\r\n * ```tsx\r\n * const state = useSyncState(ns);\r\n * if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;\r\n * ```\r\n *\r\n * `ns` deve ser o namespace retornado por `createOfflineDocumentsNamespace` (ou\r\n * qualquer objeto com `syncState` + `onSyncStateChanged`).\r\n *\r\n * Alinhado com 02-sdk.md §3.6: `useSyncState(client: NeetruClient): NeetruSyncState`.\r\n * Na camada M2, aceita diretamente o namespace de documentos (`SyncStateSource`)\r\n * pois o `NeetruClient.db` completo está fora do escopo do M2.\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useSyncState(ns: SyncStateSource): SyncState {\r\n const [state, setState] = React.useState<SyncState>(() => ns.syncState);\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n\r\n // Snapshot inicial imediato ao montar (pode ter mudado entre render e effect)\r\n if (mountedRef.current) {\r\n setState(ns.syncState);\r\n }\r\n\r\n const unsubscribe: Unsubscribe = ns.onSyncStateChanged((s: SyncState) => {\r\n if (!mountedRef.current) return;\r\n setState(s);\r\n });\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ns]);\r\n\r\n return state;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/db/react.ts"],"names":[],"mappings":"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","file":"db-react.mjs","sourcesContent":["/**\r\n * Bindings React do módulo db (M2) — `@neetru/sdk/react`.\r\n *\r\n * Exporta três hooks de tempo real que se integram com a camada offline do\r\n * `@neetru/sdk/db`:\r\n *\r\n * - `useDocument(ref, id)` — subscreve ao onSnapshot de um documento.\r\n * - `useCollection(ref, query?)` — subscreve ao onSnapshot de uma coleção.\r\n * - `useSyncState(ns)` — observa o SyncState do namespace offline.\r\n *\r\n * ### Design\r\n * Segue o padrão do subpath `/react` existente (cliente/ref passados\r\n * explicitamente, sem Context Provider obrigatório). Alinhado com 02-sdk.md §3.6.\r\n *\r\n * ### React 18/19 StrictMode\r\n * Cada hook usa um ref interno para rastrear se o componente foi desmontado,\r\n * e a função `useEffect` retorna sempre a função de unsubscribe. Em StrictMode,\r\n * o duplo-mount/unmount do React 18 chama cleanup corretamente — não há leak.\r\n *\r\n * ### Dependências\r\n * `react` é peer dependency — não importada como dep direta.\r\n */\r\nimport * as React from 'react';\r\nimport type { DbCollectionRef, DbDoc, DbQuery, DbListResult, DbGetResult } from './collection-ref';\r\nimport type { SyncState, Unsubscribe } from './offline/types';\r\nimport type { NeetruClient } from '../types';\r\n\r\n// ─── Tipos públicos dos hooks ─────────────────────────────────────────────────\r\n\r\n/**\r\n * Resultado de `useDocument`.\r\n *\r\n * - `data` — dados do documento, ou `null` se não existe / ainda carregando.\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local, não do servidor.\r\n * - `stale` — conexão caída ou sincronizando; pode haver dados mais novos.\r\n */\r\nexport interface UseDocumentResult<T> {\r\n data: T | null;\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n}\r\n\r\n/**\r\n * Resultado de `useCollection`.\r\n *\r\n * - `docs` — documentos da coleção (ou subconjunto via query).\r\n * - `loading` — `true` enquanto o primeiro snapshot não foi entregue.\r\n * - `fromCache` — o snapshot veio do cache local.\r\n * - `stale` — conexão caída ou sincronizando.\r\n * - `hasPendingWrites` — ao menos um doc tem escrita local não confirmada.\r\n */\r\nexport interface UseCollectionResult<T> {\r\n docs: DbDoc<T>[];\r\n loading: boolean;\r\n fromCache: boolean;\r\n stale: boolean;\r\n hasPendingWrites: boolean;\r\n}\r\n\r\n/**\r\n * Subconjunto mínimo do namespace offline que `useSyncState` precisa.\r\n * Compatível com `NeetruDbDocuments` de `collection-ref.ts`.\r\n *\r\n * Passado explicitamente (sem Context) para alinhar com o padrão do SDK.\r\n * Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.\r\n *\r\n * `client.db` (o `NeetruDb` retornado por `createNeetruClient`) também satisfaz\r\n * este contrato — expõe `syncState` + `onSyncStateChanged` reais. Por isso\r\n * `useSyncState` aceita tanto a fonte direta quanto o `NeetruClient` inteiro\r\n * (ver `UseSyncStateInput`).\r\n */\r\nexport interface SyncStateSource {\r\n readonly syncState: SyncState;\r\n onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;\r\n}\r\n\r\n/**\r\n * Entrada aceita por `useSyncState`:\r\n * - `NeetruClient` → resolve `client.db` automaticamente (forma oficial,\r\n * alinhada com 02-sdk.md §3.6 `useSyncState(client)`).\r\n * - `SyncStateSource` → fonte direta (`client.db` ou o namespace offline cru).\r\n */\r\nexport type UseSyncStateInput = NeetruClient | SyncStateSource;\r\n\r\n/**\r\n * Type guard — detecta se a entrada é um `NeetruClient` (tem `.db` que é, ele\r\n * próprio, um `SyncStateSource`) e não uma fonte direta. Distinção em runtime\r\n * porque os shapes não se sobrepõem: a fonte direta expõe `syncState` no nível\r\n * de topo; o client expõe via `.db`.\r\n */\r\nfunction isNeetruClient(input: UseSyncStateInput): input is NeetruClient {\r\n const maybe = input as { db?: unknown; syncState?: unknown };\r\n // Fonte direta já tem `syncState` no topo → não é o client wrapper.\r\n if (typeof maybe.syncState !== 'undefined') return false;\r\n const db = maybe.db as { syncState?: unknown; onSyncStateChanged?: unknown } | undefined;\r\n return (\r\n !!db &&\r\n typeof db.syncState !== 'undefined' &&\r\n typeof db.onSyncStateChanged === 'function'\r\n );\r\n}\r\n\r\n/** Normaliza qualquer entrada aceita para a `SyncStateSource` subjacente. */\r\nfunction resolveSyncStateSource(input: UseSyncStateInput): SyncStateSource {\r\n return isNeetruClient(input) ? (input.db as unknown as SyncStateSource) : input;\r\n}\r\n\r\n// ─── Serialização estável de query ────────────────────────────────────────────\r\n\r\n/**\r\n * Serializa uma DbQuery para string estável, usada como chave de efeito.\r\n *\r\n * `useCollection` re-subscreve somente quando a serialização muda, NÃO por\r\n * identidade de objeto. Isso evita a armadilha clássica de inline-object em\r\n * JSX que causaria re-subscribe em todo render.\r\n */\r\nfunction serializeQuery(query: DbQuery | undefined): string {\r\n if (!query) return '';\r\n try {\r\n return JSON.stringify(query, Object.keys(query).sort());\r\n } catch {\r\n return String(query);\r\n }\r\n}\r\n\r\n// ─── useDocument ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para um documento específico.\r\n *\r\n * Subscreve a `ref.doc(id).onSnapshot` no mount. Re-subscreve quando `ref`\r\n * ou `id` mudam. Cancela a subscrição no unmount.\r\n *\r\n * ```tsx\r\n * const { data, loading, fromCache, stale } = useDocument(ordersRef, orderId);\r\n * if (loading) return <Spinner />;\r\n * if (!data) return <NotFound />;\r\n * return <OrderCard order={data} fromCache={fromCache} />;\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode: a cleanup function do useEffect cancela\r\n * o listener no unmount, e o double-mount do StrictMode não vaza estado.\r\n */\r\nexport function useDocument<T>(\r\n ref: DbCollectionRef<T>,\r\n id: string,\r\n): UseDocumentResult<T> {\r\n const [state, setState] = React.useState<UseDocumentResult<T>>({\r\n data: null,\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n });\r\n\r\n // Ref para detectar unmount e evitar setState após cleanup.\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset para loading quando ref/id muda\r\n setState({ data: null, loading: true, fromCache: true, stale: false });\r\n\r\n const docRef = ref.doc(id);\r\n\r\n const unsubscribe: Unsubscribe = docRef.onSnapshot(\r\n (snap: DbGetResult<T> | null) => {\r\n if (!mountedRef.current) return;\r\n if (snap === null || snap.docs.length === 0) {\r\n setState({\r\n data: null,\r\n loading: false,\r\n fromCache: snap?.fromCache ?? true,\r\n stale: snap?.stale ?? false,\r\n });\r\n } else {\r\n setState({\r\n data: snap.docs[0]?.data ?? null,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n });\r\n }\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, id]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useCollection ────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook de tempo real para uma coleção (ou subconjunto filtrado).\r\n *\r\n * Subscreve a `ref.onSnapshot(query, cb)` no mount. Re-subscreve quando `ref`\r\n * ou a serialização de `query` muda — NÃO por identidade de objeto, o que\r\n * evita re-subscribe desnecessário em inline objects.\r\n *\r\n * ```tsx\r\n * const { docs, loading, hasPendingWrites } = useCollection(ordersRef, {\r\n * where: [{ field: 'status', op: '==', value: 'open' }],\r\n * });\r\n * ```\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useCollection<T>(\r\n ref: DbCollectionRef<T>,\r\n query?: DbQuery,\r\n): UseCollectionResult<T> {\r\n const [state, setState] = React.useState<UseCollectionResult<T>>({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n // Serializamos a query para garantir que mudanças de valor (não de referência)\r\n // triggam o re-subscribe, mas que objetos iguais não causem loop.\r\n const queryKey = serializeQuery(query);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n // Reset loading ao trocar de query/ref\r\n setState({\r\n docs: [],\r\n loading: true,\r\n fromCache: true,\r\n stale: false,\r\n hasPendingWrites: false,\r\n });\r\n\r\n const unsubscribe: Unsubscribe = ref.onSnapshot(\r\n query,\r\n (snap: DbListResult<T>) => {\r\n if (!mountedRef.current) return;\r\n setState({\r\n docs: snap.docs,\r\n loading: false,\r\n fromCache: snap.fromCache,\r\n stale: snap.stale,\r\n hasPendingWrites: snap.hasPendingWrites,\r\n });\r\n },\r\n );\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ref, queryKey]);\r\n\r\n return state;\r\n}\r\n\r\n// ─── useSyncState ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Hook para observar o `SyncState` da camada offline em tempo real.\r\n *\r\n * Retorna o `SyncState` atual e re-renderiza quando ele muda. Usado para\r\n * renderizar indicadores \"sincronizando…\" / \"offline\" / contadores de escritas\r\n * pendentes na UI.\r\n *\r\n * Aceita duas formas de entrada (SDK-QW-4 — wiring ao client real):\r\n *\r\n * ```tsx\r\n * // Forma oficial — passe o client; o hook resolve client.db internamente.\r\n * const client = createNeetruClient({ apiKey });\r\n * const state = useSyncState(client);\r\n *\r\n * // Forma direta — passe a fonte (client.db ou o namespace offline cru).\r\n * const state = useSyncState(client.db);\r\n *\r\n * if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;\r\n * ```\r\n *\r\n * Antes desta versão o hook só aceitava a `SyncStateSource` crua, o que levava\r\n * consumers a stub-arem o estado pra sempre `'idle'` em vez de ligar no client.\r\n * Agora `useSyncState(client)` (a forma documentada em 02-sdk.md §3.6) funciona\r\n * de ponta a ponta — `client.db.syncState` / `client.db.onSyncStateChanged` são\r\n * reais.\r\n *\r\n * Seguro em React 18/19 StrictMode.\r\n */\r\nexport function useSyncState(input: UseSyncStateInput): SyncState {\r\n // Resolve uma única vez por render; se a identidade da fonte resolvida mudar,\r\n // o efeito abaixo re-subscreve (dependência `source`).\r\n const source = resolveSyncStateSource(input);\r\n\r\n const [state, setState] = React.useState<SyncState>(() => source.syncState);\r\n\r\n const mountedRef = React.useRef(true);\r\n\r\n React.useEffect(() => {\r\n mountedRef.current = true;\r\n\r\n // Snapshot inicial imediato ao montar (pode ter mudado entre render e effect)\r\n if (mountedRef.current) {\r\n setState(source.syncState);\r\n }\r\n\r\n const unsubscribe: Unsubscribe = source.onSyncStateChanged((s: SyncState) => {\r\n if (!mountedRef.current) return;\r\n setState(s);\r\n });\r\n\r\n return () => {\r\n mountedRef.current = false;\r\n unsubscribe();\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [source]);\r\n\r\n return state;\r\n}\r\n"]}
package/dist/db.cjs CHANGED
@@ -1352,7 +1352,71 @@ var MutationQueue = class {
1352
1352
  }
1353
1353
  };
1354
1354
 
1355
+ // src/db/field-value.ts
1356
+ var NEETRU_SENTINEL_KEY = "__neetru__";
1357
+ function serverTimestamp() {
1358
+ return { __neetru__: "serverTimestamp" };
1359
+ }
1360
+ function increment(n) {
1361
+ if (typeof n !== "number" || !Number.isFinite(n)) {
1362
+ throw new TypeError(
1363
+ `increment(n): n deve ser um n\xFAmero finito (recebido: ${String(n)}).`
1364
+ );
1365
+ }
1366
+ return { __neetru__: "increment", operand: n };
1367
+ }
1368
+ function isFieldSentinel(v) {
1369
+ return typeof v === "object" && v !== null && NEETRU_SENTINEL_KEY in v && typeof v[NEETRU_SENTINEL_KEY] === "string";
1370
+ }
1371
+ function isServerTimestampSentinel(v) {
1372
+ return isFieldSentinel(v) && v.__neetru__ === "serverTimestamp";
1373
+ }
1374
+ function isIncrementSentinel(v) {
1375
+ return isFieldSentinel(v) && v.__neetru__ === "increment" && typeof v.operand === "number" && Number.isFinite(v.operand);
1376
+ }
1377
+ function resolveSentinelLocally(value, baseValue) {
1378
+ if (isServerTimestampSentinel(value)) {
1379
+ return Date.now();
1380
+ }
1381
+ if (isIncrementSentinel(value)) {
1382
+ const base = typeof baseValue === "number" && Number.isFinite(baseValue) ? baseValue : 0;
1383
+ return base + value.operand;
1384
+ }
1385
+ return value;
1386
+ }
1387
+ function resolveSentinelsLocally(data, existing) {
1388
+ let touched = false;
1389
+ const out = {};
1390
+ for (const [k, v] of Object.entries(data)) {
1391
+ if (isFieldSentinel(v)) {
1392
+ touched = true;
1393
+ out[k] = resolveSentinelLocally(v, existing?.[k]);
1394
+ } else {
1395
+ out[k] = v;
1396
+ }
1397
+ }
1398
+ return touched ? out : data;
1399
+ }
1400
+ function hasFieldSentinel(data) {
1401
+ for (const v of Object.values(data)) {
1402
+ if (isFieldSentinel(v)) return true;
1403
+ }
1404
+ return false;
1405
+ }
1406
+
1355
1407
  // src/db/offline/sync-engine.ts
1408
+ function hasSentinels(payload) {
1409
+ return payload !== null && hasFieldSentinel(payload);
1410
+ }
1411
+ function sanitizeSentinelPayload(payload) {
1412
+ if (payload === null) return null;
1413
+ if (!hasFieldSentinel(payload)) return payload;
1414
+ const out = {};
1415
+ for (const [k, v] of Object.entries(payload)) {
1416
+ if (!isFieldSentinel(v)) out[k] = v;
1417
+ }
1418
+ return out;
1419
+ }
1356
1420
  var SyncEngine = class {
1357
1421
  _store;
1358
1422
  _queue;
@@ -1560,11 +1624,16 @@ var SyncEngine = class {
1560
1624
  }
1561
1625
  return;
1562
1626
  }
1563
- const newData = mut.payload ?? (existing?.data ?? {});
1627
+ const literalPayload = sanitizeSentinelPayload(mut.payload);
1628
+ const newData = literalPayload ?? (existing?.data ?? {});
1564
1629
  const updatedDoc = {
1565
1630
  collection,
1566
1631
  id: docId,
1567
- data: mut.op === "update" ? { ...existing?.data ?? {}, ...newData } : newData,
1632
+ data: mut.op === "update" ? { ...existing?.data ?? {}, ...newData } : (
1633
+ // add/set: se o payload tinha sentinels, o cache otimista (existing.data)
1634
+ // é a fonte de verdade até o pull; senão usa o payload literal.
1635
+ hasSentinels(mut.payload) ? { ...existing?.data ?? {}, ...newData } : newData
1636
+ ),
1568
1637
  meta: {
1569
1638
  serverVersion,
1570
1639
  updatedAtServer: serverTimestamp2,
@@ -2646,54 +2715,6 @@ var TabCoordinator = class {
2646
2715
 
2647
2716
  // src/db/collection-ref.ts
2648
2717
  init_db_errors();
2649
-
2650
- // src/db/field-value.ts
2651
- var NEETRU_SENTINEL_KEY = "__neetru__";
2652
- function serverTimestamp() {
2653
- return { __neetru__: "serverTimestamp" };
2654
- }
2655
- function increment(n) {
2656
- if (typeof n !== "number" || !Number.isFinite(n)) {
2657
- throw new TypeError(
2658
- `increment(n): n deve ser um n\xFAmero finito (recebido: ${String(n)}).`
2659
- );
2660
- }
2661
- return { __neetru__: "increment", operand: n };
2662
- }
2663
- function isFieldSentinel(v) {
2664
- return typeof v === "object" && v !== null && NEETRU_SENTINEL_KEY in v && typeof v[NEETRU_SENTINEL_KEY] === "string";
2665
- }
2666
- function isServerTimestampSentinel(v) {
2667
- return isFieldSentinel(v) && v.__neetru__ === "serverTimestamp";
2668
- }
2669
- function isIncrementSentinel(v) {
2670
- return isFieldSentinel(v) && v.__neetru__ === "increment" && typeof v.operand === "number" && Number.isFinite(v.operand);
2671
- }
2672
- function resolveSentinelLocally(value, baseValue) {
2673
- if (isServerTimestampSentinel(value)) {
2674
- return Date.now();
2675
- }
2676
- if (isIncrementSentinel(value)) {
2677
- const base = typeof baseValue === "number" && Number.isFinite(baseValue) ? baseValue : 0;
2678
- return base + value.operand;
2679
- }
2680
- return value;
2681
- }
2682
- function resolveSentinelsLocally(data, existing) {
2683
- let touched = false;
2684
- const out = {};
2685
- for (const [k, v] of Object.entries(data)) {
2686
- if (isFieldSentinel(v)) {
2687
- touched = true;
2688
- out[k] = resolveSentinelLocally(v, existing?.[k]);
2689
- } else {
2690
- out[k] = v;
2691
- }
2692
- }
2693
- return touched ? out : data;
2694
- }
2695
-
2696
- // src/db/collection-ref.ts
2697
2718
  function isIndexedDBUnavailable() {
2698
2719
  return typeof indexedDB === "undefined" || typeof window === "undefined";
2699
2720
  }