@neetru/sdk 1.1.1 → 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 -214
- package/README.md +194 -218
- package/dist/auth.cjs +4181 -346
- 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 +4181 -346
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs +63 -24
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +6 -2
- package/dist/catalog.d.ts +6 -2
- package/dist/catalog.mjs +63 -24
- package/dist/catalog.mjs.map +1 -1
- package/dist/checkout.cjs +60 -18
- 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 +60 -18
- 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 +3652 -143
- 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 +3649 -143
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.cjs +101 -24
- package/dist/entitlements.cjs.map +1 -1
- package/dist/entitlements.d.cts +15 -5
- package/dist/entitlements.d.ts +15 -5
- package/dist/entitlements.mjs +101 -24
- 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 +4341 -282
- 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 +4243 -189
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +186 -9
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +21 -6
- package/dist/mocks.d.ts +21 -6
- package/dist/mocks.mjs +186 -9
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.cjs +296 -0
- package/dist/notifications.cjs.map +1 -0
- package/dist/notifications.d.cts +5 -0
- package/dist/notifications.d.ts +5 -0
- package/dist/notifications.mjs +293 -0
- package/dist/notifications.mjs.map +1 -0
- package/dist/react.cjs +7 -3
- 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 +7 -3
- package/dist/react.mjs.map +1 -1
- package/dist/support.cjs +60 -18
- 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 +60 -18
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.cjs +130 -19
- package/dist/telemetry.cjs.map +1 -1
- package/dist/telemetry.d.cts +21 -1
- package/dist/telemetry.d.ts +21 -1
- package/dist/telemetry.mjs +130 -19
- 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 +60 -18
- 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 +60 -18
- package/dist/usage.mjs.map +1 -1
- package/dist/webhooks.cjs +316 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +5 -0
- package/dist/webhooks.d.ts +5 -0
- package/dist/webhooks.mjs +312 -0
- package/dist/webhooks.mjs.map +1 -0
- package/package.json +133 -101
- package/dist/types-BA53dd8S.d.cts +0 -490
- package/dist/types-BA53dd8S.d.ts +0 -490
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos internos da camada offline do @neetru/sdk.
|
|
3
|
+
*
|
|
4
|
+
* Estes tipos SÃO internos ao módulo `db/offline/` — não fazem parte da
|
|
5
|
+
* superfície pública do SDK. A superfície pública é o `DbCollectionRef` e
|
|
6
|
+
* o `DbSnapshot` de `src/types.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Derivados de:
|
|
9
|
+
* - I3-sdk-offline.md §3 (store schemas)
|
|
10
|
+
* - I3-sdk-offline.md §4 (fila de escritas)
|
|
11
|
+
* - I3-sdk-offline.md §5 (query descriptor)
|
|
12
|
+
* - I3-sdk-offline.md §7 (conflito LWW)
|
|
13
|
+
* - 02-sdk.md §3.2 (DbSnapshot, DbWhereFilter, DbQuery)
|
|
14
|
+
*/
|
|
15
|
+
/** Identificador de um documento (string opaca). */
|
|
16
|
+
type DocId = string;
|
|
17
|
+
/** Identificador de uma mutação (UUID v4 client-generated). */
|
|
18
|
+
type MutationId = string;
|
|
19
|
+
/** Operações possíveis na fila de escritas. */
|
|
20
|
+
type MutationKind = 'add' | 'set' | 'update' | 'remove';
|
|
21
|
+
/**
|
|
22
|
+
* Uma mutação enfileirada (escrita durável pendente).
|
|
23
|
+
* Equivalente a `QueuedMutation` do I3 §3.3 (`mutations` store).
|
|
24
|
+
*/
|
|
25
|
+
interface Mutation {
|
|
26
|
+
/** Número de sequência autoincrement — define a ordem total da fila. */
|
|
27
|
+
seq: number;
|
|
28
|
+
/** UUID v4 client-generated — chave de idempotência. */
|
|
29
|
+
mutationId: MutationId;
|
|
30
|
+
/** Nome da coleção. */
|
|
31
|
+
collection: string;
|
|
32
|
+
/** ID do documento alvo (client-gen para `add`, existente para os demais). */
|
|
33
|
+
docId: DocId;
|
|
34
|
+
/** Operação. */
|
|
35
|
+
op: MutationKind;
|
|
36
|
+
/** Dados da operação. `null` para `remove`. */
|
|
37
|
+
payload: Record<string, unknown> | null;
|
|
38
|
+
/**
|
|
39
|
+
* `serverVersion` que o cliente viu ao escrever.
|
|
40
|
+
* `null` para `add` (doc não existia) ou quando o cliente nunca sincronizou.
|
|
41
|
+
* Usado pelo servidor para detectar conflito (I3 §7.4).
|
|
42
|
+
*/
|
|
43
|
+
baseVersion: string | null;
|
|
44
|
+
/** Epoch ms do relógio do CLIENTE — só para diagnóstico e ordenação local. */
|
|
45
|
+
enqueuedAt: number;
|
|
46
|
+
/** Número de tentativas de replay. */
|
|
47
|
+
attempts: number;
|
|
48
|
+
/** Último erro de replay (string). `null` se nenhuma tentativa ainda. */
|
|
49
|
+
lastError: string | null;
|
|
50
|
+
/** Estado de envio desta mutação. */
|
|
51
|
+
status: 'queued' | 'inflight' | 'failed';
|
|
52
|
+
/** Agrupa mutações de um `batch()` atômico. `null` se mutação individual. */
|
|
53
|
+
batchId: string | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Razão pela qual uma escrita foi descartada no LWW (I3 §3.3 `conflict_log`).
|
|
57
|
+
*/
|
|
58
|
+
type ConflictReason = 'lww_server_newer' | 'rejected_permission' | 'rejected_validation';
|
|
59
|
+
/**
|
|
60
|
+
* Registro de uma escrita perdedora — gravado no `conflict_log` (I3 §3.3).
|
|
61
|
+
* Entregue ao produto via `onWriteResult` (status: `'superseded'`).
|
|
62
|
+
*/
|
|
63
|
+
interface ConflictRecord {
|
|
64
|
+
id?: number;
|
|
65
|
+
collection: string;
|
|
66
|
+
docId: DocId;
|
|
67
|
+
mutationId: MutationId;
|
|
68
|
+
losingData: Record<string, unknown>;
|
|
69
|
+
winningData: Record<string, unknown>;
|
|
70
|
+
reason: ConflictReason;
|
|
71
|
+
detectedAt: number;
|
|
72
|
+
delivered: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Estado de sincronização da camada offline (I3 §10 / 02-sdk §3.4).
|
|
76
|
+
* Exposto em `client.db.syncState`.
|
|
77
|
+
*
|
|
78
|
+
* - `idle` — online e em repouso (sem sync ativo).
|
|
79
|
+
* - `syncing` — sync em progresso (push + pull + realtime).
|
|
80
|
+
* - `offline` — conexão perdida ou inexistente.
|
|
81
|
+
* - `error` — erro persistente após retries (reservado para uso futuro).
|
|
82
|
+
*/
|
|
83
|
+
type SyncStatus = 'idle' | 'offline' | 'syncing' | 'error';
|
|
84
|
+
interface SyncState {
|
|
85
|
+
status: SyncStatus;
|
|
86
|
+
pendingWrites: number;
|
|
87
|
+
lastSyncedAt: number | null;
|
|
88
|
+
isLeaderTab: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Função para cancelar uma subscrição (onSnapshot, onDoc, onSyncStateChanged). */
|
|
91
|
+
type Unsubscribe = () => void;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* SyncEngine — orquestrador da camada offline do @neetru/sdk.
|
|
95
|
+
*
|
|
96
|
+
* Implementa as 3 fases de sincronização definidas em I3 §6:
|
|
97
|
+
*
|
|
98
|
+
* FASE 1 — DRENAR A FILA (push)
|
|
99
|
+
* Envia mutações pendentes ao Core via SyncTransport.
|
|
100
|
+
* Cada mutação é marcada inflight, enviada e depois:
|
|
101
|
+
* - CONFIRMADA → removida da fila (markApplied)
|
|
102
|
+
* - SUPERADA (LWW)→ ConflictRecord gravado, cache atualizado
|
|
103
|
+
* - REJEITADA → resolveRejected + ConflictRecord + removida
|
|
104
|
+
* - FALHA TRANSIENTE → markRetry + PARA a drenagem (ordem preservada)
|
|
105
|
+
*
|
|
106
|
+
* FASE 2 — RECONCILIAR O CACHE (pull)
|
|
107
|
+
* Busca mudanças do servidor desde o watermark ou resume token.
|
|
108
|
+
* Cada documento passa pelo ConflictResolver (LWW) e é gravado no LocalStore.
|
|
109
|
+
* Changes são emitidos pelo ChangeBus.
|
|
110
|
+
* Se `resyncRequired` → aciona full resync em vez de pull incremental.
|
|
111
|
+
*
|
|
112
|
+
* FASE 3 — REABRIR LISTENERS (realtime)
|
|
113
|
+
* Atualiza SyncState para refletir que o cache está reconciliado.
|
|
114
|
+
* O ChangeBus já foi alimentado nas fases anteriores — os listeners
|
|
115
|
+
* ativos na camada superior (onSnapshot/onDoc) recebem snapshots frescos.
|
|
116
|
+
*
|
|
117
|
+
* Garantias:
|
|
118
|
+
* - SOMENTE a aba LÍDER executa o sync (TabCoordinator.isLeader()).
|
|
119
|
+
* - Re-entrante-safe: um sync em progresso bloqueia novos disparos.
|
|
120
|
+
* - Fail-closed: falha transiente retém a mutação; falha de pull não
|
|
121
|
+
* corrompe o cache nem o watermark.
|
|
122
|
+
* - Idempotente: o SyncTransport usa mutationId como chave de idempotência.
|
|
123
|
+
*
|
|
124
|
+
* O SyncTransport é INJETADO — a ligação real com o Core acontece na camada
|
|
125
|
+
* de adaptação, não aqui. Isso torna o SyncEngine 100% testável com fake.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Documento retornado pelo servidor em push/pull/resync.
|
|
130
|
+
*/
|
|
131
|
+
interface ServerDoc {
|
|
132
|
+
collection: string;
|
|
133
|
+
id: string;
|
|
134
|
+
data: Record<string, unknown>;
|
|
135
|
+
serverVersion: string;
|
|
136
|
+
serverTimestamp: number;
|
|
137
|
+
/** `true` se o doc foi deletado no servidor. */
|
|
138
|
+
deleted: boolean;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resultado individual de uma mutação enviada ao Core (FASE 1).
|
|
142
|
+
*/
|
|
143
|
+
type MutationResult = {
|
|
144
|
+
mutationId: string;
|
|
145
|
+
outcome: 'confirmed';
|
|
146
|
+
serverVersion: string;
|
|
147
|
+
serverTimestamp: number;
|
|
148
|
+
} | {
|
|
149
|
+
mutationId: string;
|
|
150
|
+
outcome: 'superseded';
|
|
151
|
+
serverVersion: string;
|
|
152
|
+
serverTimestamp: number;
|
|
153
|
+
serverData: Record<string, unknown>;
|
|
154
|
+
} | {
|
|
155
|
+
mutationId: string;
|
|
156
|
+
outcome: 'rejected';
|
|
157
|
+
reason: 'rejected_permission' | 'rejected_validation';
|
|
158
|
+
serverVersion: string | null;
|
|
159
|
+
serverData: Record<string, unknown> | null;
|
|
160
|
+
};
|
|
161
|
+
/** Retorno de pushMutations. */
|
|
162
|
+
interface PushMutationsResult {
|
|
163
|
+
results: MutationResult[];
|
|
164
|
+
}
|
|
165
|
+
/** Retorno de pullChanges. */
|
|
166
|
+
interface PullChangesResult {
|
|
167
|
+
docs: ServerDoc[];
|
|
168
|
+
newWatermark: number | null;
|
|
169
|
+
/** `true` quando o resume token / watermark ficou inválido (gap grande). */
|
|
170
|
+
resyncRequired: boolean;
|
|
171
|
+
}
|
|
172
|
+
/** Retorno de fullResync. */
|
|
173
|
+
interface FullResyncResult {
|
|
174
|
+
docs: ServerDoc[];
|
|
175
|
+
newWatermark: number | null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Contrato do transporte de sync — injetado no SyncEngine.
|
|
179
|
+
*
|
|
180
|
+
* A implementação real usa os endpoints `/api/sdk/v1/db/*` do Core.
|
|
181
|
+
* Em testes, usa-se um FakeSyncTransport.
|
|
182
|
+
*
|
|
183
|
+
* Per I3 §6:
|
|
184
|
+
* - `pushMutations` → FASE 1: envia mutações em ordem de seq.
|
|
185
|
+
* - `pullChanges` → FASE 2: busca delta desde o watermark/resumeToken.
|
|
186
|
+
* - `fullResync` → FASE 2 (fallback): lista completa das coleções ativas.
|
|
187
|
+
*/
|
|
188
|
+
interface SyncTransport {
|
|
189
|
+
/**
|
|
190
|
+
* Envia um lote de mutações ao Core.
|
|
191
|
+
* O Core aplica, verifica conflito LWW e retorna um resultado por mutação.
|
|
192
|
+
* A ordem do array deve ser preservada (seq crescente).
|
|
193
|
+
*/
|
|
194
|
+
pushMutations(mutations: Mutation[]): Promise<PushMutationsResult>;
|
|
195
|
+
/**
|
|
196
|
+
* Busca documentos modificados no servidor desde `sinceWatermark`.
|
|
197
|
+
* `sinceWatermark === null` indica primeiro sync (sem base conhecida).
|
|
198
|
+
* `resumeToken` é o token de change stream (nosql-vm); pode ser null.
|
|
199
|
+
*
|
|
200
|
+
* Retorna `resyncRequired: true` se o gap é grande demais para pull incremental.
|
|
201
|
+
*/
|
|
202
|
+
pullChanges(sinceWatermark: number | null, resumeToken: string | null): Promise<PullChangesResult>;
|
|
203
|
+
/**
|
|
204
|
+
* Faz um full resync das coleções informadas.
|
|
205
|
+
* Retorna TODOS os documentos atuais (paginado internamente pelo transporte).
|
|
206
|
+
* Docs ausentes na resposta foram deletados durante o gap.
|
|
207
|
+
*/
|
|
208
|
+
fullResync(collections: string[]): Promise<FullResyncResult>;
|
|
209
|
+
/**
|
|
210
|
+
* (Opcional) Registra uma subscription realtime para uma coleção específica.
|
|
211
|
+
*
|
|
212
|
+
* Implementado pelo WebSocket transport (nosql-vm): chama
|
|
213
|
+
* `NeetruRealtimeClient.subscribe()` e alimenta os frames delta/resync
|
|
214
|
+
* no `ChangeBus` da camada offline.
|
|
215
|
+
*
|
|
216
|
+
* Transportes REST e Firestore não implementam este método (undefined).
|
|
217
|
+
* O chamador (`DbCollectionRefImpl.onSnapshot`) verifica antes de invocar.
|
|
218
|
+
*
|
|
219
|
+
* @param collection - Nome da coleção a subscrever.
|
|
220
|
+
* @param onChange - Callback que recebe changes quando um delta/resync chega.
|
|
221
|
+
* `null` payload = resync full necessário.
|
|
222
|
+
* @returns Função de unsubscribe.
|
|
223
|
+
*/
|
|
224
|
+
subscribeCollection?: (collection: string, onChange: (changes: Array<{
|
|
225
|
+
type: 'added' | 'modified' | 'removed';
|
|
226
|
+
docId: string;
|
|
227
|
+
data: Record<string, unknown> | null;
|
|
228
|
+
}>, needsResync: boolean) => void) => () => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* DbCollectionRef — superfície de Documentos offline-first do @neetru/sdk (M2).
|
|
233
|
+
*
|
|
234
|
+
* Implementa a API pública conforme especificada em:
|
|
235
|
+
* - 02-sdk.md §3.2 (contrato TypeScript DbCollectionRef)
|
|
236
|
+
* - I2-mundo-documentos.md §3.2 (superfície dupla CRUD + realtime)
|
|
237
|
+
* - I3-sdk-offline.md §10 (camada offline, lifecycle, SyncState)
|
|
238
|
+
*
|
|
239
|
+
* ### Semântica de Promise de escrita (02-sdk.md §3.2 nota negrito)
|
|
240
|
+
* `add/set/update/remove` resolvem quando a escrita está **durável localmente**
|
|
241
|
+
* (cache + fila), NÃO quando o servidor confirma. Isso é offline-first por design:
|
|
242
|
+
* sem rede, `add()` resolve imediatamente. Para saber quando sincronizou, o produto
|
|
243
|
+
* observa `onWriteResult` (future / SyncEngine). A Promise só rejeita por:
|
|
244
|
+
* - validação síncrona (nome de coleção inválido, id vazio)
|
|
245
|
+
* - `offline_quota_exceeded` (IndexedDB cheio)
|
|
246
|
+
*
|
|
247
|
+
* ### Injeção do transporte realtime
|
|
248
|
+
* O `RealtimeTransport` é injetado via `createOfflineDocumentsNamespace({ transport })`.
|
|
249
|
+
* A implementação atual usa o `SyncEngine` para push/pull (sync offline ↔ servidor).
|
|
250
|
+
* Transportes futuros (Firestore Web SDK / WebSocket gateway) são injetados aqui —
|
|
251
|
+
* sem alterar a superfície pública.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
interface DbWhereFilter {
|
|
255
|
+
field: string;
|
|
256
|
+
op: '==' | '!=' | '<' | '<=' | '>' | '>=' | 'array-contains' | 'in' | 'not-in';
|
|
257
|
+
value: unknown;
|
|
258
|
+
}
|
|
259
|
+
interface DbQuery {
|
|
260
|
+
where?: DbWhereFilter[];
|
|
261
|
+
orderBy?: {
|
|
262
|
+
field: string;
|
|
263
|
+
direction?: 'asc' | 'desc';
|
|
264
|
+
};
|
|
265
|
+
/** Máximo de documentos. Default 20, max 500. */
|
|
266
|
+
limit?: number;
|
|
267
|
+
/** Cursor opaco — devolvido em `DbListResult.nextCursor`. */
|
|
268
|
+
cursor?: string;
|
|
269
|
+
}
|
|
270
|
+
interface DbDoc<T = Record<string, unknown>> {
|
|
271
|
+
id: string;
|
|
272
|
+
data: T;
|
|
273
|
+
}
|
|
274
|
+
interface DbListResult<T = Record<string, unknown>> {
|
|
275
|
+
docs: DbDoc<T>[];
|
|
276
|
+
nextCursor: string | null;
|
|
277
|
+
fromCache: boolean;
|
|
278
|
+
stale: boolean;
|
|
279
|
+
hasPendingWrites: boolean;
|
|
280
|
+
changes: Array<{
|
|
281
|
+
type: 'added' | 'modified' | 'removed';
|
|
282
|
+
doc: DbDoc<T>;
|
|
283
|
+
}>;
|
|
284
|
+
}
|
|
285
|
+
interface DbGetResult<T = Record<string, unknown>> {
|
|
286
|
+
docs: Array<DbDoc<T>>;
|
|
287
|
+
fromCache: boolean;
|
|
288
|
+
stale: boolean;
|
|
289
|
+
hasPendingWrites: boolean;
|
|
290
|
+
changes: Array<{
|
|
291
|
+
type: 'added' | 'modified' | 'removed';
|
|
292
|
+
doc: DbDoc<T>;
|
|
293
|
+
}>;
|
|
294
|
+
}
|
|
295
|
+
type DbChangeType = 'added' | 'modified' | 'removed';
|
|
296
|
+
interface DbBatchOp {
|
|
297
|
+
op: 'add' | 'set' | 'update' | 'remove';
|
|
298
|
+
collection: string;
|
|
299
|
+
id?: string;
|
|
300
|
+
data?: Record<string, unknown>;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Contrato do transporte de realtime — injetado no namespace.
|
|
304
|
+
*
|
|
305
|
+
* A implementação real usa os endpoints `/api/sdk/v1/db/*` do Core.
|
|
306
|
+
* Em testes, usa-se um fake in-memory.
|
|
307
|
+
*
|
|
308
|
+
* Este é o **seam de injeção** que conecta a camada offline ao servidor real.
|
|
309
|
+
* Transportes futuros (Firestore Web SDK direto / WebSocket gateway) são
|
|
310
|
+
* implementados aqui, sem modificar a superfície pública `DbCollectionRef`.
|
|
311
|
+
*/
|
|
312
|
+
type RealtimeTransport = SyncTransport;
|
|
313
|
+
/**
|
|
314
|
+
* Referência a um documento específico.
|
|
315
|
+
* Parte da superfície `DbCollectionRef.doc(id)`.
|
|
316
|
+
*/
|
|
317
|
+
interface DbDocRef<T = Record<string, unknown>> {
|
|
318
|
+
get(): Promise<DbGetResult<T> | null>;
|
|
319
|
+
set(data: Omit<T, 'id'>): Promise<{
|
|
320
|
+
ok: true;
|
|
321
|
+
}>;
|
|
322
|
+
update(data: Partial<Omit<T, 'id'>>): Promise<{
|
|
323
|
+
ok: true;
|
|
324
|
+
}>;
|
|
325
|
+
remove(): Promise<{
|
|
326
|
+
ok: true;
|
|
327
|
+
}>;
|
|
328
|
+
onSnapshot(cb: (snap: DbGetResult<T> | null) => void): Unsubscribe;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Referência a uma coleção — superfície dupla CRUD + realtime.
|
|
332
|
+
*
|
|
333
|
+
* Conforme 02-sdk.md §3.2 / I2 §3.2.
|
|
334
|
+
*/
|
|
335
|
+
interface DbCollectionRef<T = Record<string, unknown>> {
|
|
336
|
+
/** Retorna snapshot de um documento. `null` se não existir (nem no cache). */
|
|
337
|
+
get(id: string): Promise<DbGetResult<T> | null>;
|
|
338
|
+
/** Lista documentos com query opcional. */
|
|
339
|
+
list(q?: DbQuery): Promise<DbListResult<T>>;
|
|
340
|
+
/** Adiciona doc com id gerado pelo cliente (UUID v4). Durável localmente. */
|
|
341
|
+
add(data: Omit<T, 'id'>): Promise<{
|
|
342
|
+
ok: true;
|
|
343
|
+
id: string;
|
|
344
|
+
}>;
|
|
345
|
+
/** Cria ou sobrescreve um doc pelo id. Durável localmente. */
|
|
346
|
+
set(id: string, data: Omit<T, 'id'>): Promise<{
|
|
347
|
+
ok: true;
|
|
348
|
+
}>;
|
|
349
|
+
/** Atualiza campos de um doc (merge). Durável localmente. */
|
|
350
|
+
update(id: string, data: Partial<Omit<T, 'id'>>): Promise<{
|
|
351
|
+
ok: true;
|
|
352
|
+
}>;
|
|
353
|
+
/** Remove um doc (tombstone local). Durável localmente. */
|
|
354
|
+
remove(id: string): Promise<{
|
|
355
|
+
ok: true;
|
|
356
|
+
}>;
|
|
357
|
+
/** Aplica múltiplas operações atomicamente na fila. */
|
|
358
|
+
batch(ops: DbBatchOp[]): Promise<{
|
|
359
|
+
ok: true;
|
|
360
|
+
}>;
|
|
361
|
+
/** Escuta um documento específico. */
|
|
362
|
+
onDoc(id: string, cb: (doc: T | null) => void): Unsubscribe;
|
|
363
|
+
/** Escuta a coleção inteira (ou subconjunto via query). */
|
|
364
|
+
onSnapshot(q: DbQuery | undefined, cb: (snap: DbListResult<T>) => void): Unsubscribe;
|
|
365
|
+
/** Retorna uma referência ao documento `id`. */
|
|
366
|
+
doc(id: string): DbDocRef<T>;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Namespace `db.documents` — ponto de entrada para coleções de documentos.
|
|
370
|
+
*
|
|
371
|
+
* Analogia a `NeetruDb` de 02-sdk.md §3.1 mas escoped para o Mundo Documentos.
|
|
372
|
+
*/
|
|
373
|
+
interface NeetruDbDocuments {
|
|
374
|
+
collection<T = Record<string, unknown>>(name: string): DbCollectionRef<T>;
|
|
375
|
+
/** Estado atual de sincronização (offline/online/syncing). */
|
|
376
|
+
readonly syncState: SyncState;
|
|
377
|
+
onSyncStateChanged(cb: (s: SyncState) => void): Unsubscribe;
|
|
378
|
+
/** Força flush da fila de mutações pendentes. */
|
|
379
|
+
flush(): Promise<void>;
|
|
380
|
+
/** Limpa o cache local (IndexedDB) — operação destrutiva. */
|
|
381
|
+
clearCache(): Promise<void>;
|
|
382
|
+
/**
|
|
383
|
+
* Retorna os registros do log de conflitos LWW gravados pelo SyncEngine.
|
|
384
|
+
* Um conflito ocorre quando a mutação local é descartada porque o servidor
|
|
385
|
+
* já avançou a versão do documento (Last-Write-Wins).
|
|
386
|
+
*/
|
|
387
|
+
getConflicts(): Promise<ConflictRecord[]>;
|
|
388
|
+
}
|
|
389
|
+
interface CreateOfflineDocumentsOptions {
|
|
390
|
+
/**
|
|
391
|
+
* Nome do banco IndexedDB — deve ser único por produto × dbId × env.
|
|
392
|
+
* Convenção: `neetru-db__{productSlug}__{dbId}__{env}`.
|
|
393
|
+
*/
|
|
394
|
+
dbName: string;
|
|
395
|
+
/**
|
|
396
|
+
* Transporte de sync — implementa `SyncTransport` do SyncEngine.
|
|
397
|
+
* Este é o **seam de injeção** do transporte real.
|
|
398
|
+
*
|
|
399
|
+
* Implementações possíveis:
|
|
400
|
+
* - `RestSyncTransport` — REST → Core (padrão em staging/prod)
|
|
401
|
+
* - `FirestoreRealtimeTransport` — Firestore Web SDK direto (engine=firestore)
|
|
402
|
+
* - `WebSocketTransport` — WebSocket → gateway (engine=nosql-vm)
|
|
403
|
+
* - `FakeRealtimeTransport` — in-memory (testes)
|
|
404
|
+
*/
|
|
405
|
+
transport: RealtimeTransport;
|
|
406
|
+
/**
|
|
407
|
+
* Estado inicial de conectividade.
|
|
408
|
+
* Default: `navigator.onLine` se disponível, senão `true`.
|
|
409
|
+
*/
|
|
410
|
+
startOnline?: boolean;
|
|
411
|
+
/**
|
|
412
|
+
* `true` = sem contenção multi-aba (sempre líder).
|
|
413
|
+
* Padrão em testes; em produção usa Web Locks.
|
|
414
|
+
*/
|
|
415
|
+
singleTab?: boolean;
|
|
416
|
+
/**
|
|
417
|
+
* Intervalo do sync periódico em ms. `0` desativa.
|
|
418
|
+
* Default: 5 * 60 * 1000 (5min).
|
|
419
|
+
*/
|
|
420
|
+
periodicSyncIntervalMs?: number;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export type { ConflictRecord as C, DbBatchOp as D, NeetruDbDocuments as N, RealtimeTransport as R, SyncState as S, Unsubscribe as U, CreateOfflineDocumentsOptions as a, DbChangeType as b, DbCollectionRef as c, DbDoc as d, DbDocRef as e, DbGetResult as f, DbListResult as g, DbQuery as h, DbWhereFilter as i };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
|
|
5
|
+
function _interopNamespace(e) {
|
|
6
|
+
if (e && e.__esModule) return e;
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
24
|
+
|
|
25
|
+
// src/db/react.ts
|
|
26
|
+
function serializeQuery(query) {
|
|
27
|
+
if (!query) return "";
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(query, Object.keys(query).sort());
|
|
30
|
+
} catch {
|
|
31
|
+
return String(query);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function useDocument(ref, id) {
|
|
35
|
+
const [state, setState] = React__namespace.useState({
|
|
36
|
+
data: null,
|
|
37
|
+
loading: true,
|
|
38
|
+
fromCache: true,
|
|
39
|
+
stale: false
|
|
40
|
+
});
|
|
41
|
+
const mountedRef = React__namespace.useRef(true);
|
|
42
|
+
React__namespace.useEffect(() => {
|
|
43
|
+
mountedRef.current = true;
|
|
44
|
+
setState({ data: null, loading: true, fromCache: true, stale: false });
|
|
45
|
+
const docRef = ref.doc(id);
|
|
46
|
+
const unsubscribe = docRef.onSnapshot(
|
|
47
|
+
(snap) => {
|
|
48
|
+
if (!mountedRef.current) return;
|
|
49
|
+
if (snap === null || snap.docs.length === 0) {
|
|
50
|
+
setState({
|
|
51
|
+
data: null,
|
|
52
|
+
loading: false,
|
|
53
|
+
fromCache: snap?.fromCache ?? true,
|
|
54
|
+
stale: snap?.stale ?? false
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
setState({
|
|
58
|
+
data: snap.docs[0]?.data ?? null,
|
|
59
|
+
loading: false,
|
|
60
|
+
fromCache: snap.fromCache,
|
|
61
|
+
stale: snap.stale
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
return () => {
|
|
67
|
+
mountedRef.current = false;
|
|
68
|
+
unsubscribe();
|
|
69
|
+
};
|
|
70
|
+
}, [ref, id]);
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
function useCollection(ref, query) {
|
|
74
|
+
const [state, setState] = React__namespace.useState({
|
|
75
|
+
docs: [],
|
|
76
|
+
loading: true,
|
|
77
|
+
fromCache: true,
|
|
78
|
+
stale: false,
|
|
79
|
+
hasPendingWrites: false
|
|
80
|
+
});
|
|
81
|
+
const mountedRef = React__namespace.useRef(true);
|
|
82
|
+
const queryKey = serializeQuery(query);
|
|
83
|
+
React__namespace.useEffect(() => {
|
|
84
|
+
mountedRef.current = true;
|
|
85
|
+
setState({
|
|
86
|
+
docs: [],
|
|
87
|
+
loading: true,
|
|
88
|
+
fromCache: true,
|
|
89
|
+
stale: false,
|
|
90
|
+
hasPendingWrites: false
|
|
91
|
+
});
|
|
92
|
+
const unsubscribe = ref.onSnapshot(
|
|
93
|
+
query,
|
|
94
|
+
(snap) => {
|
|
95
|
+
if (!mountedRef.current) return;
|
|
96
|
+
setState({
|
|
97
|
+
docs: snap.docs,
|
|
98
|
+
loading: false,
|
|
99
|
+
fromCache: snap.fromCache,
|
|
100
|
+
stale: snap.stale,
|
|
101
|
+
hasPendingWrites: snap.hasPendingWrites
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
return () => {
|
|
106
|
+
mountedRef.current = false;
|
|
107
|
+
unsubscribe();
|
|
108
|
+
};
|
|
109
|
+
}, [ref, queryKey]);
|
|
110
|
+
return state;
|
|
111
|
+
}
|
|
112
|
+
function useSyncState(ns) {
|
|
113
|
+
const [state, setState] = React__namespace.useState(() => ns.syncState);
|
|
114
|
+
const mountedRef = React__namespace.useRef(true);
|
|
115
|
+
React__namespace.useEffect(() => {
|
|
116
|
+
mountedRef.current = true;
|
|
117
|
+
if (mountedRef.current) {
|
|
118
|
+
setState(ns.syncState);
|
|
119
|
+
}
|
|
120
|
+
const unsubscribe = ns.onSyncStateChanged((s) => {
|
|
121
|
+
if (!mountedRef.current) return;
|
|
122
|
+
setState(s);
|
|
123
|
+
});
|
|
124
|
+
return () => {
|
|
125
|
+
mountedRef.current = false;
|
|
126
|
+
unsubscribe();
|
|
127
|
+
};
|
|
128
|
+
}, [ns]);
|
|
129
|
+
return state;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
exports.useCollection = useCollection;
|
|
133
|
+
exports.useDocument = useDocument;
|
|
134
|
+
exports.useSyncState = useSyncState;
|
|
135
|
+
//# sourceMappingURL=db-react.cjs.map
|
|
136
|
+
//# sourceMappingURL=db-react.cjs.map
|
|
@@ -0,0 +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"]}
|
|
@@ -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.cjs';
|
|
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 };
|