@neetru/sdk 3.0.1 → 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.
- package/CHANGELOG.md +554 -492
- package/dist/auth.cjs +84 -15
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -2
- package/dist/auth.d.ts +1 -2
- package/dist/auth.mjs +84 -15
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.d.cts +1 -2
- package/dist/catalog.d.ts +1 -2
- package/dist/checkout.d.cts +1 -2
- package/dist/checkout.d.ts +1 -2
- package/dist/db-react.cjs +15 -5
- package/dist/db-react.cjs.map +1 -1
- package/dist/db-react.d.cts +35 -13
- package/dist/db-react.d.ts +35 -13
- package/dist/db-react.mjs +15 -5
- package/dist/db-react.mjs.map +1 -1
- package/dist/db.cjs +104 -18
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -2
- package/dist/db.d.ts +1 -2
- package/dist/db.mjs +99 -19
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.d.cts +1 -2
- package/dist/entitlements.d.ts +1 -2
- package/dist/firestore-compat.cjs +399 -0
- package/dist/firestore-compat.cjs.map +1 -0
- package/dist/firestore-compat.d.cts +294 -0
- package/dist/firestore-compat.d.ts +294 -0
- package/dist/firestore-compat.mjs +371 -0
- package/dist/firestore-compat.mjs.map +1 -0
- package/dist/index.cjs +105 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.mjs +100 -20
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.d.cts +1 -2
- package/dist/mocks.d.ts +1 -2
- package/dist/notifications.d.cts +1 -2
- package/dist/notifications.d.ts +1 -2
- package/dist/react.cjs +267 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +117 -3
- package/dist/react.d.ts +117 -3
- package/dist/react.mjs +265 -1
- package/dist/react.mjs.map +1 -1
- package/dist/support.d.cts +1 -2
- package/dist/support.d.ts +1 -2
- package/dist/telemetry.d.cts +1 -2
- package/dist/telemetry.d.ts +1 -2
- package/dist/{types-DALIhcbq.d.ts → types-BRv8wBxX.d.ts} +581 -2
- package/dist/{types-Lfd3LiAF.d.cts → types-nwErcRX8.d.cts} +581 -2
- package/dist/usage.d.cts +1 -2
- package/dist/usage.d.ts +1 -2
- package/dist/webhooks.d.cts +1 -2
- package/dist/webhooks.d.ts +1 -2
- package/package.json +156 -151
- package/dist/collection-ref-DqAAhuhX.d.cts +0 -472
- package/dist/collection-ref-DqAAhuhX.d.ts +0 -472
package/dist/catalog.d.cts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import './collection-ref-DqAAhuhX.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 {
|
|
2
|
-
import './collection-ref-DqAAhuhX.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
|
|
package/dist/checkout.d.cts
CHANGED
|
@@ -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,
|
|
2
|
-
import './collection-ref-DqAAhuhX.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';
|
package/dist/checkout.d.ts
CHANGED
|
@@ -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,
|
|
2
|
-
import './collection-ref-DqAAhuhX.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(
|
|
113
|
-
const
|
|
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(
|
|
128
|
+
setState(source.syncState);
|
|
119
129
|
}
|
|
120
|
-
const unsubscribe =
|
|
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
|
-
}, [
|
|
138
|
+
}, [source]);
|
|
129
139
|
return state;
|
|
130
140
|
}
|
|
131
141
|
|
package/dist/db-react.cjs.map
CHANGED
|
@@ -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"]}
|
package/dist/db-react.d.cts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* pois o `NeetruClient.db` completo está fora do escopo do M2.
|
|
111
|
+
* Antes desta versão o hook só 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(
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* pois o `NeetruClient.db` completo está fora do escopo do M2.
|
|
111
|
+
* Antes desta versão o hook só 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(
|
|
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(
|
|
91
|
-
const
|
|
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(
|
|
106
|
+
setState(source.syncState);
|
|
97
107
|
}
|
|
98
|
-
const unsubscribe =
|
|
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
|
-
}, [
|
|
116
|
+
}, [source]);
|
|
107
117
|
return state;
|
|
108
118
|
}
|
|
109
119
|
|
package/dist/db-react.mjs.map
CHANGED
|
@@ -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"]}
|