@neetru/sdk 1.2.0 → 2.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 +284 -244
- package/README.md +194 -194
- package/dist/auth.cjs +3740 -345
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +5 -1
- package/dist/auth.d.ts +5 -1
- package/dist/auth.mjs +3740 -345
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +5 -1
- package/dist/catalog.d.ts +5 -1
- package/dist/catalog.mjs.map +1 -1
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +5 -1
- package/dist/checkout.d.ts +5 -1
- package/dist/checkout.mjs.map +1 -1
- package/dist/collection-ref-BBvTTXoG.d.cts +423 -0
- package/dist/collection-ref-BBvTTXoG.d.ts +423 -0
- package/dist/db-react.cjs +136 -0
- package/dist/db-react.cjs.map +1 -0
- package/dist/db-react.d.cts +99 -0
- package/dist/db-react.d.ts +99 -0
- package/dist/db-react.mjs +112 -0
- package/dist/db-react.mjs.map +1 -0
- package/dist/db.cjs +3599 -131
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +5 -8
- package/dist/db.d.ts +5 -8
- package/dist/db.mjs +3596 -131
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.cjs.map +1 -1
- package/dist/entitlements.d.cts +5 -1
- package/dist/entitlements.d.ts +5 -1
- package/dist/entitlements.mjs.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/index.cjs +3957 -342
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -6
- package/dist/index.d.ts +13 -6
- package/dist/index.mjs +3877 -263
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +183 -7
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +18 -5
- package/dist/mocks.d.ts +18 -5
- package/dist/mocks.mjs +183 -7
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.cjs.map +1 -1
- package/dist/notifications.d.cts +5 -1
- package/dist/notifications.d.ts +5 -1
- package/dist/notifications.mjs.map +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +5 -1
- package/dist/react.d.ts +5 -1
- package/dist/react.mjs.map +1 -1
- package/dist/support.cjs.map +1 -1
- package/dist/support.d.cts +5 -1
- package/dist/support.d.ts +5 -1
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.cjs.map +1 -1
- package/dist/telemetry.d.cts +5 -1
- package/dist/telemetry.d.ts +5 -1
- package/dist/telemetry.mjs.map +1 -1
- package/dist/types-B1jylbMC.d.ts +1364 -0
- package/dist/types-Kmt4y1FQ.d.cts +1364 -0
- package/dist/usage.cjs.map +1 -1
- package/dist/usage.d.cts +5 -1
- package/dist/usage.d.ts +5 -1
- package/dist/usage.mjs.map +1 -1
- package/dist/webhooks.cjs.map +1 -1
- package/dist/webhooks.d.cts +5 -1
- package/dist/webhooks.d.ts +5 -1
- package/dist/webhooks.mjs.map +1 -1
- package/package.json +133 -111
- package/dist/types-CQAfwqUS.d.cts +0 -654
- package/dist/types-CQAfwqUS.d.ts +0 -654
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { S as SyncState, U as Unsubscribe, d as DbDoc, c as DbCollectionRef, h as DbQuery } from './collection-ref-BBvTTXoG.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resultado de `useDocument`.
|
|
5
|
+
*
|
|
6
|
+
* - `data` — dados do documento, ou `null` se não existe / ainda carregando.
|
|
7
|
+
* - `loading` — `true` enquanto o primeiro snapshot não foi entregue.
|
|
8
|
+
* - `fromCache` — o snapshot veio do cache local, não do servidor.
|
|
9
|
+
* - `stale` — conexão caída ou sincronizando; pode haver dados mais novos.
|
|
10
|
+
*/
|
|
11
|
+
interface UseDocumentResult<T> {
|
|
12
|
+
data: T | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
fromCache: boolean;
|
|
15
|
+
stale: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resultado de `useCollection`.
|
|
19
|
+
*
|
|
20
|
+
* - `docs` — documentos da coleção (ou subconjunto via query).
|
|
21
|
+
* - `loading` — `true` enquanto o primeiro snapshot não foi entregue.
|
|
22
|
+
* - `fromCache` — o snapshot veio do cache local.
|
|
23
|
+
* - `stale` — conexão caída ou sincronizando.
|
|
24
|
+
* - `hasPendingWrites` — ao menos um doc tem escrita local não confirmada.
|
|
25
|
+
*/
|
|
26
|
+
interface UseCollectionResult<T> {
|
|
27
|
+
docs: DbDoc<T>[];
|
|
28
|
+
loading: boolean;
|
|
29
|
+
fromCache: boolean;
|
|
30
|
+
stale: boolean;
|
|
31
|
+
hasPendingWrites: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Subconjunto mínimo do namespace offline que `useSyncState` precisa.
|
|
35
|
+
* Compatível com `NeetruDbDocuments` de `collection-ref.ts`.
|
|
36
|
+
*
|
|
37
|
+
* Passado explicitamente (sem Context) para alinhar com o padrão do SDK.
|
|
38
|
+
* Tipicamente: o namespace retornado por `createOfflineDocumentsNamespace`.
|
|
39
|
+
*/
|
|
40
|
+
interface SyncStateSource {
|
|
41
|
+
readonly syncState: SyncState;
|
|
42
|
+
onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hook de tempo real para um documento específico.
|
|
46
|
+
*
|
|
47
|
+
* Subscreve a `ref.doc(id).onSnapshot` no mount. Re-subscreve quando `ref`
|
|
48
|
+
* ou `id` mudam. Cancela a subscrição no unmount.
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const { data, loading, fromCache, stale } = useDocument(ordersRef, orderId);
|
|
52
|
+
* if (loading) return <Spinner />;
|
|
53
|
+
* if (!data) return <NotFound />;
|
|
54
|
+
* return <OrderCard order={data} fromCache={fromCache} />;
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Seguro em React 18/19 StrictMode: a cleanup function do useEffect cancela
|
|
58
|
+
* o listener no unmount, e o double-mount do StrictMode não vaza estado.
|
|
59
|
+
*/
|
|
60
|
+
declare function useDocument<T>(ref: DbCollectionRef<T>, id: string): UseDocumentResult<T>;
|
|
61
|
+
/**
|
|
62
|
+
* Hook de tempo real para uma coleção (ou subconjunto filtrado).
|
|
63
|
+
*
|
|
64
|
+
* Subscreve a `ref.onSnapshot(query, cb)` no mount. Re-subscreve quando `ref`
|
|
65
|
+
* ou a serialização de `query` muda — NÃO por identidade de objeto, o que
|
|
66
|
+
* evita re-subscribe desnecessário em inline objects.
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const { docs, loading, hasPendingWrites } = useCollection(ordersRef, {
|
|
70
|
+
* where: [{ field: 'status', op: '==', value: 'open' }],
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* Seguro em React 18/19 StrictMode.
|
|
75
|
+
*/
|
|
76
|
+
declare function useCollection<T>(ref: DbCollectionRef<T>, query?: DbQuery): UseCollectionResult<T>;
|
|
77
|
+
/**
|
|
78
|
+
* Hook para observar o SyncState do namespace offline.
|
|
79
|
+
*
|
|
80
|
+
* Retorna o `SyncState` atual e re-renderiza quando ele muda.
|
|
81
|
+
* Usado para renderizar indicadores "sincronizando…" ou "offline" na UI.
|
|
82
|
+
*
|
|
83
|
+
* ```tsx
|
|
84
|
+
* const state = useSyncState(ns);
|
|
85
|
+
* if (state.status === 'offline') return <OfflineBanner pending={state.pendingWrites} />;
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
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.
|
|
94
|
+
*
|
|
95
|
+
* Seguro em React 18/19 StrictMode.
|
|
96
|
+
*/
|
|
97
|
+
declare function useSyncState(ns: SyncStateSource): SyncState;
|
|
98
|
+
|
|
99
|
+
export { type SyncStateSource, type UseCollectionResult, type UseDocumentResult, useCollection, useDocument, useSyncState };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
// src/db/react.ts
|
|
4
|
+
function serializeQuery(query) {
|
|
5
|
+
if (!query) return "";
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(query, Object.keys(query).sort());
|
|
8
|
+
} catch {
|
|
9
|
+
return String(query);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function useDocument(ref, id) {
|
|
13
|
+
const [state, setState] = React.useState({
|
|
14
|
+
data: null,
|
|
15
|
+
loading: true,
|
|
16
|
+
fromCache: true,
|
|
17
|
+
stale: false
|
|
18
|
+
});
|
|
19
|
+
const mountedRef = React.useRef(true);
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
mountedRef.current = true;
|
|
22
|
+
setState({ data: null, loading: true, fromCache: true, stale: false });
|
|
23
|
+
const docRef = ref.doc(id);
|
|
24
|
+
const unsubscribe = docRef.onSnapshot(
|
|
25
|
+
(snap) => {
|
|
26
|
+
if (!mountedRef.current) return;
|
|
27
|
+
if (snap === null || snap.docs.length === 0) {
|
|
28
|
+
setState({
|
|
29
|
+
data: null,
|
|
30
|
+
loading: false,
|
|
31
|
+
fromCache: snap?.fromCache ?? true,
|
|
32
|
+
stale: snap?.stale ?? false
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
setState({
|
|
36
|
+
data: snap.docs[0]?.data ?? null,
|
|
37
|
+
loading: false,
|
|
38
|
+
fromCache: snap.fromCache,
|
|
39
|
+
stale: snap.stale
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
return () => {
|
|
45
|
+
mountedRef.current = false;
|
|
46
|
+
unsubscribe();
|
|
47
|
+
};
|
|
48
|
+
}, [ref, id]);
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
function useCollection(ref, query) {
|
|
52
|
+
const [state, setState] = React.useState({
|
|
53
|
+
docs: [],
|
|
54
|
+
loading: true,
|
|
55
|
+
fromCache: true,
|
|
56
|
+
stale: false,
|
|
57
|
+
hasPendingWrites: false
|
|
58
|
+
});
|
|
59
|
+
const mountedRef = React.useRef(true);
|
|
60
|
+
const queryKey = serializeQuery(query);
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
mountedRef.current = true;
|
|
63
|
+
setState({
|
|
64
|
+
docs: [],
|
|
65
|
+
loading: true,
|
|
66
|
+
fromCache: true,
|
|
67
|
+
stale: false,
|
|
68
|
+
hasPendingWrites: false
|
|
69
|
+
});
|
|
70
|
+
const unsubscribe = ref.onSnapshot(
|
|
71
|
+
query,
|
|
72
|
+
(snap) => {
|
|
73
|
+
if (!mountedRef.current) return;
|
|
74
|
+
setState({
|
|
75
|
+
docs: snap.docs,
|
|
76
|
+
loading: false,
|
|
77
|
+
fromCache: snap.fromCache,
|
|
78
|
+
stale: snap.stale,
|
|
79
|
+
hasPendingWrites: snap.hasPendingWrites
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
return () => {
|
|
84
|
+
mountedRef.current = false;
|
|
85
|
+
unsubscribe();
|
|
86
|
+
};
|
|
87
|
+
}, [ref, queryKey]);
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
function useSyncState(ns) {
|
|
91
|
+
const [state, setState] = React.useState(() => ns.syncState);
|
|
92
|
+
const mountedRef = React.useRef(true);
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
mountedRef.current = true;
|
|
95
|
+
if (mountedRef.current) {
|
|
96
|
+
setState(ns.syncState);
|
|
97
|
+
}
|
|
98
|
+
const unsubscribe = ns.onSyncStateChanged((s) => {
|
|
99
|
+
if (!mountedRef.current) return;
|
|
100
|
+
setState(s);
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
mountedRef.current = false;
|
|
104
|
+
unsubscribe();
|
|
105
|
+
};
|
|
106
|
+
}, [ns]);
|
|
107
|
+
return state;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { useCollection, useDocument, useSyncState };
|
|
111
|
+
//# sourceMappingURL=db-react.mjs.map
|
|
112
|
+
//# sourceMappingURL=db-react.mjs.map
|
|
@@ -0,0 +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"]}
|