reflectdb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1260 -0
  3. package/dist/cjs/client/index.cjs +1252 -0
  4. package/dist/cjs/client/index.d.cts +629 -0
  5. package/dist/cjs/client/storage/indexeddb.cjs +253 -0
  6. package/dist/cjs/client/storage/indexeddb.d.cts +85 -0
  7. package/dist/cjs/core/index.cjs +202 -0
  8. package/dist/cjs/core/index.d.cts +473 -0
  9. package/dist/cjs/react/index.cjs +1512 -0
  10. package/dist/cjs/react/index.d.cts +748 -0
  11. package/dist/cjs/server/drizzle.cjs +413 -0
  12. package/dist/cjs/server/drizzle.d.cts +233 -0
  13. package/dist/cjs/server/index.cjs +4486 -0
  14. package/dist/cjs/server/index.d.cts +1988 -0
  15. package/dist/cjs/svelte/index.cjs +1414 -0
  16. package/dist/cjs/svelte/index.d.cts +645 -0
  17. package/dist/cjs/transport/bun-ws.cjs +244 -0
  18. package/dist/cjs/transport/bun-ws.d.cts +215 -0
  19. package/dist/cjs/transport/polling.cjs +285 -0
  20. package/dist/cjs/transport/polling.d.cts +210 -0
  21. package/dist/cjs/transport/sse.cjs +312 -0
  22. package/dist/cjs/transport/sse.d.cts +205 -0
  23. package/dist/cjs/transport/ws.cjs +330 -0
  24. package/dist/cjs/transport/ws.d.cts +236 -0
  25. package/dist/cjs/vanilla/index.cjs +1430 -0
  26. package/dist/cjs/vanilla/index.d.cts +657 -0
  27. package/dist/client/index.d.ts +629 -0
  28. package/dist/client/index.js +106 -0
  29. package/dist/client/storage/indexeddb.d.ts +85 -0
  30. package/dist/client/storage/indexeddb.js +213 -0
  31. package/dist/core/index.d.ts +473 -0
  32. package/dist/core/index.js +56 -0
  33. package/dist/react/index.d.ts +748 -0
  34. package/dist/react/index.js +366 -0
  35. package/dist/server/drizzle.d.ts +233 -0
  36. package/dist/server/drizzle.js +9 -0
  37. package/dist/server/index.d.ts +1988 -0
  38. package/dist/server/index.js +3959 -0
  39. package/dist/shared/esm-3tkwvysa.js +54 -0
  40. package/dist/shared/esm-b7xs9cde.js +4 -0
  41. package/dist/shared/esm-rw7jjtrv.js +58 -0
  42. package/dist/shared/esm-wkwx6bd9.js +25 -0
  43. package/dist/shared/esm-ytrd3hbq.js +1007 -0
  44. package/dist/shared/esm-z1xse19c.js +369 -0
  45. package/dist/svelte/index.d.ts +645 -0
  46. package/dist/svelte/index.js +262 -0
  47. package/dist/transport/bun-ws.d.ts +215 -0
  48. package/dist/transport/bun-ws.js +140 -0
  49. package/dist/transport/polling.d.ts +210 -0
  50. package/dist/transport/polling.js +181 -0
  51. package/dist/transport/sse.d.ts +205 -0
  52. package/dist/transport/sse.js +208 -0
  53. package/dist/transport/ws.d.ts +236 -0
  54. package/dist/transport/ws.js +226 -0
  55. package/dist/vanilla/index.d.ts +657 -0
  56. package/dist/vanilla/index.js +278 -0
  57. package/package.json +253 -0
@@ -0,0 +1,748 @@
1
+ import { Context, ReactNode } from "react";
2
+ type OpType = "insert" | "update" | "delete";
3
+ type OpStatus = "pending" | "synced" | "rejected";
4
+ /**
5
+ * Server-side conflict policy for concurrent writes to the same row.
6
+ *
7
+ * Scope note for `"merge"`: the server resolves per column using the **client
8
+ * op HLCs** recorded in reflectdb's mirror, so two clients editing different
9
+ * fields both land. The per-column clocks the client sees on a broadcast are a
10
+ * different domain — a diff-driven broadcast can't attribute a column to the
11
+ * op that produced it, so every column changed in one broadcast carries that
12
+ * broadcast's HLC. Client-side merge therefore orders *broadcasts* against each
13
+ * other and against local optimistic state; it does not reconstruct per-column
14
+ * causality between clients. Column-level convergence is a server guarantee.
15
+ */
16
+ type ConflictPolicy = "lww" | "merge" | "server" | CustomConflictPolicy;
17
+ interface CustomConflictPolicy {
18
+ policy: "custom";
19
+ resolve: ConflictResolver;
20
+ }
21
+ interface ConflictResolver {
22
+ (incoming: {
23
+ op: OpType;
24
+ payload: Record<string, unknown> | null;
25
+ hlc: string;
26
+ }, existing: {
27
+ row: Record<string, unknown>;
28
+ colClocks: Record<string, string>;
29
+ }, meta: {
30
+ table: string;
31
+ rowId: string;
32
+ userId: string;
33
+ }): {
34
+ row: Record<string, unknown>;
35
+ };
36
+ }
37
+ type ErrorReason = "outside_shape" | "readonly_query" | "rate_limited" | "buffer_full" | "server_conflict" | "custom_conflict" | "merge_stale" | "clock_drift" | "replay" | "batch_too_large" | "schema_outdated" | "auth_revoked" | "compacted" | "mutation_rejected" | "server_error" | "unknown_query";
38
+ interface HelloMessage {
39
+ type: "hello";
40
+ /** Highest protocol version the client prefers. Retained for back-compat. */
41
+ protocolVersion: number;
42
+ /** All versions the client can speak. Omit for legacy single-version clients. */
43
+ supportedVersions?: readonly number[];
44
+ clientId: string;
45
+ token: string;
46
+ }
47
+ interface BootstrapMessage {
48
+ type: "bootstrap";
49
+ syncParams?: Record<string, Record<string, unknown>>;
50
+ }
51
+ interface ResumeMessage {
52
+ type: "resume";
53
+ since: string;
54
+ }
55
+ interface OpsMessage {
56
+ type: "ops";
57
+ ops: ClientOp[];
58
+ token: string;
59
+ }
60
+ interface ClientOp {
61
+ id: string;
62
+ table: string;
63
+ op: OpType;
64
+ rowId: string;
65
+ payload: Record<string, unknown> | null;
66
+ hlc: string;
67
+ batchId?: string;
68
+ batchSize?: number;
69
+ batchSeq?: number;
70
+ }
71
+ interface SyncDeclareMessage {
72
+ type: "sync_declare";
73
+ table: string;
74
+ params?: Record<string, unknown>;
75
+ window?: number;
76
+ }
77
+ interface LoadMoreMessage {
78
+ type: "load_more";
79
+ table: string;
80
+ count: number;
81
+ }
82
+ interface UnsyncMessage {
83
+ type: "unsync";
84
+ table: string;
85
+ }
86
+ interface AuthMessage {
87
+ type: "auth";
88
+ token: string;
89
+ }
90
+ interface EphemeralMessage {
91
+ type: "ephemeral";
92
+ key: string;
93
+ userId: string;
94
+ data: Record<string, unknown>;
95
+ ttlMs?: number;
96
+ }
97
+ interface HelloAckMessage {
98
+ type: "hello_ack";
99
+ protocolVersion: number;
100
+ serverId: string;
101
+ }
102
+ interface HelloRejectMessage {
103
+ type: "hello_reject";
104
+ reason: string;
105
+ supported: number[];
106
+ }
107
+ interface SnapshotMessage {
108
+ type: "snapshot";
109
+ table: string;
110
+ rows: Record<string, unknown>[];
111
+ colClocks: Record<string, Record<string, string>>;
112
+ append?: boolean;
113
+ totalCount?: number;
114
+ /**
115
+ * Primary key column name for this table. Self-contained per message so the
116
+ * client doesn't depend on `bootstrap_complete.tableMeta` arriving first.
117
+ * When omitted (older servers), the client falls back to tableMeta, then "id".
118
+ */
119
+ pk?: string;
120
+ }
121
+ interface BootstrapCompleteMessage {
122
+ type: "bootstrap_complete";
123
+ serverHlc: string;
124
+ tableMeta: Record<string, TableMeta>;
125
+ }
126
+ interface TableMeta {
127
+ serverSet: string[];
128
+ readonly: string[];
129
+ broadcast: "consistent" | "eager" | "eager-durable";
130
+ /** Primary key column name. Defaults to "id" when omitted (older servers). */
131
+ pk?: string;
132
+ }
133
+ interface DeltaMessage {
134
+ type: "delta";
135
+ table: string;
136
+ op: OpType;
137
+ rowId: string;
138
+ payload: Record<string, unknown> | null;
139
+ hlc: string;
140
+ colClocks?: Record<string, string>;
141
+ }
142
+ interface AckMessage {
143
+ type: "ack";
144
+ opIds: string[];
145
+ }
146
+ interface RejectMessage {
147
+ type: "reject";
148
+ opId?: string;
149
+ batchId?: string;
150
+ reason: ErrorReason;
151
+ serverRow?: Record<string, unknown>;
152
+ }
153
+ interface ResumeCompleteMessage {
154
+ type: "resume_complete";
155
+ serverHlc: string;
156
+ }
157
+ interface ResumeRejectedMessage {
158
+ type: "resume_rejected";
159
+ reason: string;
160
+ serverHlc: string;
161
+ }
162
+ interface ShapeChangedMessage {
163
+ type: "shape_changed";
164
+ table: string;
165
+ reason: "auth_changed" | "params_changed" | "server_policy";
166
+ }
167
+ interface DisconnectMessage {
168
+ type: "disconnect";
169
+ reason: string;
170
+ }
171
+ interface ReauthMessage {
172
+ type: "reauth";
173
+ }
174
+ interface CountChangedMessage {
175
+ type: "count_changed";
176
+ table: string;
177
+ totalCount: number;
178
+ }
179
+ interface EphemeralEvent {
180
+ type: "ephemeral";
181
+ key: string;
182
+ clientId: string;
183
+ userId: string;
184
+ data: Record<string, unknown>;
185
+ ttlMs?: number;
186
+ }
187
+ type ClientMessage = HelloMessage | BootstrapMessage | ResumeMessage | OpsMessage | SyncDeclareMessage | LoadMoreMessage | UnsyncMessage | AuthMessage | EphemeralMessage;
188
+ type ServerMessage = HelloAckMessage | HelloRejectMessage | SnapshotMessage | BootstrapCompleteMessage | DeltaMessage | AckMessage | RejectMessage | ResumeCompleteMessage | ResumeRejectedMessage | ShapeChangedMessage | DisconnectMessage | ReauthMessage | EphemeralEvent | CountChangedMessage;
189
+ interface ClientTransport {
190
+ send(message: ClientMessage): Promise<void>;
191
+ subscribe(handler: (message: ServerMessage) => void): void;
192
+ close(): Promise<void>;
193
+ }
194
+ interface ClientStorageAdapter {
195
+ getRows(table: string): Promise<LocalRow[]>;
196
+ getAllRows(): Promise<LocalRow[]>;
197
+ putRow(table: string, rowId: string, row: LocalRow): Promise<void>;
198
+ deleteRow(table: string, rowId: string): Promise<void>;
199
+ clearTable(table: string): Promise<void>;
200
+ getPendingOps(): Promise<PendingOp[]>;
201
+ putPendingOps(ops: PendingOp[]): Promise<void>;
202
+ appendPendingOps(ops: PendingOp[]): Promise<void>;
203
+ updatePendingOps(ops: PendingOp[]): Promise<void>;
204
+ removePendingOps(opIds: string[]): Promise<void>;
205
+ clearPendingOps(): Promise<void>;
206
+ getMeta(key: string): Promise<string | null>;
207
+ setMeta(key: string, value: string): Promise<void>;
208
+ deleteMeta(key: string): Promise<void>;
209
+ clear(): Promise<void>;
210
+ }
211
+ interface PendingOp {
212
+ op: ClientOp;
213
+ status: OpStatus;
214
+ rejectedReason: ErrorReason | null;
215
+ createdAt: number;
216
+ /**
217
+ * Row state captured just before the optimistic apply. Restored on reject
218
+ * so a rejected optimistic insert collapses back to no-row (instead of
219
+ * leaving a tombstone), and a rejected update reverts to the merged row
220
+ * the client had before touching it.
221
+ */
222
+ preState?: {
223
+ data: Record<string, unknown> | null;
224
+ colClocks: Record<string, string>;
225
+ serverHlc: string | null;
226
+ } | null;
227
+ }
228
+ interface LocalRow {
229
+ table: string;
230
+ rowId: string;
231
+ data: Record<string, unknown> | null;
232
+ colClocks: Record<string, string>;
233
+ serverHlc: string | null;
234
+ }
235
+ declare class ClientStore {
236
+ private pendingOps;
237
+ private rows;
238
+ private tableMeta;
239
+ private storage;
240
+ private pendingWrites;
241
+ private writeChain;
242
+ constructor(storage?: ClientStorageAdapter);
243
+ /**
244
+ * Load durable state into memory.
245
+ *
246
+ * @param options.tables — hydrate only these tables. Rows only ever reach
247
+ * local storage through a subscription, so restoring just the tables the
248
+ * client is actually subscribed to reads no less than it needs. Omit to
249
+ * load every row of every table the client has ever synced.
250
+ */
251
+ hydrate(options?: {
252
+ tables?: string[];
253
+ }): Promise<void>;
254
+ flush(): Promise<void>;
255
+ private enqueue;
256
+ addPendingOp(op: ClientOp): void;
257
+ getPendingOps(): PendingOp[];
258
+ markSynced(opIds: string[]): void;
259
+ markRejected(opId: string, reason: ErrorReason): void;
260
+ rejectBatch(batchId: string, reason: ErrorReason): void;
261
+ private autoTrimRejected;
262
+ clearRejected(): void;
263
+ setRow(table: string, rowId: string, data: Record<string, unknown> | null, colClocks: Record<string, string>, serverHlc: string | null): void;
264
+ getRow(table: string, rowId: string): LocalRow | undefined;
265
+ /** Internal: includes tombstones for HLC comparison in applyDelta. */
266
+ private getRowEntry;
267
+ getRows(table: string, includeDeleted?: boolean): LocalRow[];
268
+ clearTable(table: string): void;
269
+ applySnapshot(table: string, rows: Record<string, unknown>[], colClocks: Record<string, Record<string, string>>, append?: boolean, pk?: string): void;
270
+ applyDelta(table: string, op: string, rowId: string, payload: Record<string, unknown> | null, hlc: string, colClocks?: Record<string, string>): void;
271
+ revertOp(opId: string, serverRow: Record<string, unknown> | null | undefined): void;
272
+ setTableMeta(meta: Record<string, {
273
+ serverSet: string[];
274
+ readonly: string[];
275
+ broadcast?: "consistent" | "eager" | "eager-durable";
276
+ pk?: string;
277
+ }>): void;
278
+ getTableMeta(table: string): {
279
+ serverSet: string[];
280
+ readonly: string[];
281
+ broadcast: "consistent" | "eager" | "eager-durable";
282
+ pk: string;
283
+ } | undefined;
284
+ applyOptimistic(op: ClientOp): void;
285
+ clear(): Promise<void>;
286
+ }
287
+ interface SyncClientConfig {
288
+ clientId: string;
289
+ transport: ClientTransport;
290
+ token: string;
291
+ storage?: ClientStorageAdapter;
292
+ onSync?: (table: string) => void;
293
+ /**
294
+ * Structured error callback. `reason` is drawn from the protocol's
295
+ * ErrorReason taxonomy when the error originates server-side; local
296
+ * failures (reauth/transport) use a "local:*" namespace so consumers
297
+ * can switch on the string.
298
+ */
299
+ onError?: (error: {
300
+ opId?: string;
301
+ batchId?: string;
302
+ table?: string;
303
+ reason: ErrorReason | `local:${string}`;
304
+ message?: string;
305
+ }) => void;
306
+ onReauth?: () => Promise<string>;
307
+ /** Max reconnect backoff in ms (before jitter). Default: 30_000. */
308
+ maxReconnectDelayMs?: number;
309
+ /**
310
+ * Load every stored row at boot instead of only the tables this client is
311
+ * subscribed to. Needed only if you read rows for a table before calling
312
+ * `sync()` on it. Default: false.
313
+ */
314
+ hydrateAllTables?: boolean;
315
+ }
316
+ type SyncClientState = "hydrating" | "disconnected" | "connecting" | "connected" | "bootstrapping" | "synced";
317
+ interface SyncOptions {
318
+ window?: number;
319
+ }
320
+ declare class SyncClient {
321
+ private config;
322
+ private transport;
323
+ private opCreator;
324
+ private store;
325
+ private state;
326
+ /** Version agreed on in hello_ack. Null until first successful handshake. */
327
+ private negotiatedProtocolVersion;
328
+ private syncedTables;
329
+ private serverHlc;
330
+ private listeners;
331
+ private version;
332
+ private tableVersions;
333
+ private tableListeners;
334
+ private connectResolve;
335
+ private connectReject;
336
+ private connectInFlight;
337
+ private ephemeralListeners;
338
+ private totalCounts;
339
+ private syncOptions;
340
+ private bootstrapScheduled;
341
+ private initialized;
342
+ private syncParams;
343
+ private reconnectTimer;
344
+ private reconnectAttempts;
345
+ private closed;
346
+ /**
347
+ * Ops already transmitted, mapped to when they were sent. Without this,
348
+ * every mutation re-sends the whole pending set: 50 rapid edits before the
349
+ * first ack land as 1+2+…+50 = 1275 ops on the wire, each one a `reserveOp`
350
+ * insert server-side. Cleared on disconnect so a reconnect resends them.
351
+ */
352
+ private inFlightOps;
353
+ /**
354
+ * How long an op may sit unacked before a later push retries it. A frame can
355
+ * be dropped without the connection dying — a full server-side queue, for
356
+ * one — and without this the op would stay marked in-flight, never re-sent,
357
+ * until the connection happens to drop. Resends are idempotent server-side.
358
+ */
359
+ private static readonly IN_FLIGHT_TTL_MS;
360
+ /** Tail of the serialized push chain; also the coalescing slot. */
361
+ private pushChain;
362
+ private pushPending;
363
+ constructor(config: SyncClientConfig);
364
+ init(): Promise<void>;
365
+ connect(): Promise<void>;
366
+ close(): Promise<void>;
367
+ getState(): SyncClientState;
368
+ /** Version negotiated with the server. Null before the first hello_ack. */
369
+ getProtocolVersion(): number | null;
370
+ sync(table: string, params?: Record<string, unknown>, options?: SyncOptions): Promise<void>;
371
+ loadMore(table: string, count: number): Promise<void>;
372
+ getTotalCount(table: string): number | null;
373
+ unsync(table: string): Promise<void>;
374
+ bootstrap(): Promise<void>;
375
+ /**
376
+ * Schedule a bootstrap in the next microtask.
377
+ * Multiple calls within the same tick are coalesced into one bootstrap.
378
+ */
379
+ scheduleBootstrap(): void;
380
+ resume(): Promise<void>;
381
+ insert(table: string, rowId: string, payload: Record<string, unknown>): ClientOp;
382
+ update(table: string, rowId: string, payload: Record<string, unknown>): ClientOp;
383
+ delete(table: string, rowId: string): ClientOp;
384
+ batch(ops: Array<{
385
+ table: string;
386
+ op: OpType;
387
+ rowId: string;
388
+ payload: Record<string, unknown> | null;
389
+ }>): ClientOp[];
390
+ /**
391
+ * Transmit pending ops.
392
+ *
393
+ * Concurrent calls are coalesced: at most one push runs and at most one
394
+ * more is queued behind it, so a burst of mutations (every `useSync`
395
+ * mutation calls this) produces one send per op rather than one send of
396
+ * everything pending per mutation. The returned promise resolves once the
397
+ * caller's own ops have been handed to the transport.
398
+ */
399
+ push(): Promise<void>;
400
+ private doPush;
401
+ /** Forget which ops are on the wire — used when the connection drops. */
402
+ private clearInFlight;
403
+ sendEphemeral(params: {
404
+ key: string;
405
+ userId: string;
406
+ data: Record<string, unknown>;
407
+ ttlMs?: number;
408
+ }): Promise<void>;
409
+ subscribeEphemeral(key: string, listener: (event: EphemeralEvent) => void): () => void;
410
+ getRows(table: string, options?: {
411
+ includeDeleted?: boolean;
412
+ }): Record<string, unknown>[];
413
+ getRow(table: string, rowId: string): Record<string, unknown> | null;
414
+ getPendingCount(): number;
415
+ getStore(): ClientStore;
416
+ /**
417
+ * Subscribe to store changes. Returns an unsubscribe function.
418
+ * Compatible with React's useSyncExternalStore.
419
+ */
420
+ subscribe(listener: () => void): () => void;
421
+ /**
422
+ * Subscribe to changes for a specific table. Returns an unsubscribe function.
423
+ * More efficient than subscribe() — only fires when the given table changes.
424
+ */
425
+ subscribeTable(table: string, listener: () => void): () => void;
426
+ /** Returns a version number that increments on every data change. */
427
+ getVersion(): number;
428
+ /** Returns a version number for a specific table. Only increments when that table changes. */
429
+ getTableVersion(table: string): number;
430
+ private notify;
431
+ /**
432
+ * Reset client state. With no argument, wipes everything (pending ops,
433
+ * rows, server HLC, subscriptions). With `tables`, only those tables'
434
+ * rows + subscriptions are dropped — useful for logging out of a single
435
+ * scope or evicting a stale dataset without losing pending ops elsewhere.
436
+ */
437
+ clear(opts?: {
438
+ tables?: string[];
439
+ }): Promise<void>;
440
+ private handleMessage;
441
+ private mergeServerHlc;
442
+ private persistMeta;
443
+ private persistSyncSubscriptions;
444
+ private handleReauth;
445
+ private scheduleReconnect;
446
+ }
447
+ interface SyncProviderProps {
448
+ /** WebSocket URL to connect to (e.g., "ws://localhost:3001/sync") */
449
+ url: string;
450
+ /** Auth token */
451
+ token: string;
452
+ /** Tables to sync. Provider handles connect → sync → bootstrap lifecycle. */
453
+ tables?: string[];
454
+ /** Unique client ID. Auto-generated if not provided. */
455
+ clientId?: string;
456
+ /** Durable storage adapter for offline-first persistence (e.g., createIndexedDBStorage) */
457
+ storage?: ClientStorageAdapter;
458
+ /** Called when the server requests re-authentication */
459
+ onReauth?: () => Promise<string>;
460
+ /** Called on sync errors */
461
+ onError?: (error: {
462
+ opId?: string;
463
+ batchId?: string;
464
+ table?: string;
465
+ reason: ErrorReason | `local:${string}`;
466
+ message?: string;
467
+ }) => void;
468
+ children: ReactNode;
469
+ }
470
+ interface SyncContextValue {
471
+ client: SyncClient;
472
+ /** Tables managed by the provider (auto-synced) */
473
+ managedTables: Set<string>;
474
+ }
475
+ declare const SyncContext: Context<SyncContextValue | null>;
476
+ declare function SyncProvider2({ url, token, tables, clientId, storage, onReauth, onError, children }: SyncProviderProps): ReactNode;
477
+ declare function useSyncClient(): SyncClient;
478
+ /**
479
+ * Subscribe to a table and get reactive rows + mutation functions.
480
+ *
481
+ * If the table is listed in `<SyncProvider tables={[...]}>`
482
+ * the provider manages sync lifecycle. Otherwise the hook handles it.
483
+ *
484
+ * ```tsx
485
+ * function PostList() {
486
+ * const { rows, insert, update, remove } = useSync("posts");
487
+ * return rows.map(p => <div key={p.id}>{p.title}</div>);
488
+ * }
489
+ * ```
490
+ */
491
+ declare function useSync2<T extends object = Record<string, unknown>>(table: string, options?: {
492
+ params?: Record<string, unknown>;
493
+ includeDeleted?: boolean;
494
+ window?: number;
495
+ }): {
496
+ rows: T[];
497
+ insert: (rowId: string, payload: Partial<T>) => void;
498
+ update: (rowId: string, payload: Partial<T>) => void;
499
+ remove: (rowId: string) => void;
500
+ };
501
+ /**
502
+ * Get the current sync connection state.
503
+ *
504
+ * ```tsx
505
+ * function StatusBar() {
506
+ * const status = useSyncStatus();
507
+ * return <div>{status}</div>;
508
+ * }
509
+ * ```
510
+ */
511
+ declare function useSyncStatus2(): SyncClientState;
512
+ /**
513
+ * Get a single row by ID, reactively.
514
+ *
515
+ * ```tsx
516
+ * function PostDetail({ id }: { id: string }) {
517
+ * const post = useRow("posts", id);
518
+ * if (!post) return <div>Not found</div>;
519
+ * return <div>{post.title}</div>;
520
+ * }
521
+ * ```
522
+ */
523
+ declare function useRow2<T extends object = Record<string, unknown>>(table: string, rowId: string): T | null;
524
+ /**
525
+ * Get the number of pending (unsynced) operations.
526
+ */
527
+ declare function usePendingCount2(): number;
528
+ /**
529
+ * Subscribe to ephemeral events (transient, fire-and-forget).
530
+ * Returns current events by userId and a broadcast function.
531
+ *
532
+ * ```tsx
533
+ * function CursorTracker({ userId }: { userId: string }) {
534
+ * const { events, broadcast } = useEphemeral({
535
+ * key: "cursor",
536
+ * userId,
537
+ * ttlMs: 5000,
538
+ * });
539
+ *
540
+ * useEffect(() => {
541
+ * const interval = setInterval(() => {
542
+ * broadcast({ x: Math.random() * 100, y: Math.random() * 100 });
543
+ * }, 100);
544
+ * return () => clearInterval(interval);
545
+ * }, [broadcast]);
546
+ *
547
+ * return (
548
+ * <div>
549
+ * {Object.entries(events).map(([uid, data]) => (
550
+ * <div key={uid}>User {uid}: {JSON.stringify(data)}</div>
551
+ * ))}
552
+ * </div>
553
+ * );
554
+ * }
555
+ * ```
556
+ */
557
+ declare function useEphemeral2<T extends object>(config: {
558
+ key: string;
559
+ userId: string;
560
+ ttlMs?: number;
561
+ }): {
562
+ events: Record<string, T>;
563
+ broadcast: (data: T) => Promise<void>;
564
+ };
565
+ /**
566
+ * Get the total server-side count for a windowed query.
567
+ * Returns null if the query isn't windowed or count hasn't been received yet.
568
+ */
569
+ declare function useTotalCount2(table: string): number | null;
570
+ /**
571
+ * Get a loadMore callback for a windowed query.
572
+ */
573
+ declare function useLoadMore2(table: string): (count: number) => void;
574
+ import { ReactNode as ReactNode2 } from "react";
575
+ interface DrizzleTableLike {
576
+ $inferSelect: Record<string, unknown>;
577
+ $inferInsert: Record<string, unknown>;
578
+ }
579
+ /**
580
+ * Value supplied per serverSet field in the schema's object form.
581
+ * Either a static value or a function that receives the request context.
582
+ */
583
+ type ServerSetSchemaValue = unknown | ((ctx: {
584
+ auth: unknown;
585
+ params: unknown;
586
+ }) => unknown);
587
+ /**
588
+ * Schema-side `serverSet` declaration. Two shapes:
589
+ * - `string[]` — keys only; values are supplied at `implement(...)` time.
590
+ * - `Record<string, ServerSetSchemaValue>` — keys + values collocated in schema.
591
+ */
592
+ type SchemaServerSet = readonly string[] | Readonly<Record<string, ServerSetSchemaValue>>;
593
+ interface SyncQueryDef {
594
+ /** Drizzle table — row type derived from $inferSelect */
595
+ table?: DrizzleTableLike;
596
+ /** Phantom row type for non-drizzle usage: `{} as MyRow` */
597
+ row?: Record<string, unknown>;
598
+ /** Database tables for change detection. Falls back to query key name. */
599
+ tables?: string[];
600
+ /** Phantom value for params type: `{} as { orgId: string }` */
601
+ params?: Record<string, unknown>;
602
+ /** Primary key column name. Defaults to "id". Single-column only. */
603
+ pk?: string;
604
+ conflict?: ConflictPolicy;
605
+ readonly?: readonly string[];
606
+ serverSet?: SchemaServerSet;
607
+ countHints?: boolean;
608
+ }
609
+ /**
610
+ * Read-only computed query. Compiles down to `server.query(...)` with a
611
+ * throwing mutate so the type system blocks `useSync(...).insert/update/remove`
612
+ * and the runtime rejects any direct write attempt.
613
+ */
614
+ interface SyncViewDef {
615
+ __view: true;
616
+ row?: Record<string, unknown>;
617
+ params?: Record<string, unknown>;
618
+ tables?: string[];
619
+ /** Tables to watch for change-detection. Defaults to `tables` or query key. */
620
+ deps?: string[];
621
+ }
622
+ /**
623
+ * Typed ephemeral channel. Sugar over the runtime `useEphemeral` hook —
624
+ * derives a stable key from the schema name + serialized params and gives
625
+ * `peers`/`set` a typed `state`.
626
+ */
627
+ interface SyncPresenceDef {
628
+ __presence: true;
629
+ state?: Record<string, unknown>;
630
+ params?: Record<string, unknown>;
631
+ ttlMs?: number;
632
+ }
633
+ type SyncQueryEntry = SyncQueryDef | SyncViewDef | SyncPresenceDef;
634
+ type SyncQueryMap = Record<string, SyncQueryEntry>;
635
+ /** Row type: from `table.$inferSelect` if drizzle, otherwise from `row` phantom */
636
+ type InferRow<
637
+ TQueries extends SyncQueryMap,
638
+ K extends keyof TQueries
639
+ > = TQueries[K] extends {
640
+ table: DrizzleTableLike;
641
+ } ? TQueries[K]["table"]["$inferSelect"] : TQueries[K] extends {
642
+ row: infer R extends Record<string, unknown>;
643
+ } ? R : Record<string, unknown>;
644
+ /** Presence state type — typed shape passed to/received from `usePresence`. */
645
+ type InferState<
646
+ TQueries extends SyncQueryMap,
647
+ K extends keyof TQueries
648
+ > = TQueries[K] extends {
649
+ state: infer S extends Record<string, unknown>;
650
+ } ? S : Record<string, unknown>;
651
+ /** Params type. `undefined` when no params declared. */
652
+ type InferParams<
653
+ TQueries extends SyncQueryMap,
654
+ K extends keyof TQueries
655
+ > = TQueries[K] extends {
656
+ params: infer P extends Record<string, unknown>;
657
+ } ? P : undefined;
658
+ /** `true` when the query declares params */
659
+ type RequiresParams<
660
+ TQueries extends SyncQueryMap,
661
+ K extends keyof TQueries
662
+ > = TQueries[K] extends {
663
+ params: Record<string, unknown>;
664
+ } ? true : false;
665
+ /** Primary key column name. Defaults to "id". */
666
+ type PkOf<TDef> = TDef extends {
667
+ pk: infer P extends string;
668
+ } ? P : "id";
669
+ /** Extract serverSet field names from either array form or object form. */
670
+ type InferServerSetKeys<TDef> = TDef extends {
671
+ serverSet: readonly (infer S extends string)[];
672
+ } ? S : TDef extends {
673
+ serverSet: infer O extends Readonly<Record<string, unknown>>;
674
+ } ? keyof O & string : never;
675
+ /**
676
+ * Row minus readonly fields, serverSet fields, and the primary key.
677
+ * View defs collapse to `never` to block client-side writes at the type level.
678
+ * Presence defs aren't writable rows; they collapse to `never` too.
679
+ */
680
+ type InferWritableRow<
681
+ TQueries extends SyncQueryMap,
682
+ K extends keyof TQueries
683
+ > = TQueries[K] extends {
684
+ __view: true;
685
+ } | {
686
+ __presence: true;
687
+ } ? never : Omit<InferRow<TQueries, K>, (TQueries[K] extends {
688
+ readonly: readonly (infer R extends string)[];
689
+ } ? R : never) | InferServerSetKeys<TQueries[K]> | PkOf<TQueries[K]>>;
690
+ /**
691
+ * useSync return shape. View entries collapse to a read-only `{ rows }`
692
+ * (plus a `loading` flag) so the type system blocks `insert/update/remove`.
693
+ */
694
+ type UseSyncResult<
695
+ TQueries extends SyncQueryMap,
696
+ K extends keyof TQueries
697
+ > = TQueries[K] extends SyncViewDef ? {
698
+ rows: InferRow<TQueries, K>[];
699
+ loading: boolean;
700
+ } : {
701
+ rows: InferRow<TQueries, K>[];
702
+ insert: (rowId: string, payload: InferWritableRow<TQueries, K>) => void;
703
+ update: (rowId: string, payload: Partial<InferWritableRow<TQueries, K>>) => void;
704
+ remove: (rowId: string) => void;
705
+ };
706
+ /** Keys whose value is a `SyncPresenceDef`. */
707
+ type PresenceKeys<TQueries extends SyncQueryMap> = { [K in keyof TQueries] : TQueries[K] extends SyncPresenceDef ? K : never }[keyof TQueries];
708
+ /** Derive a stable presence key from name + serialized params. */
709
+ declare function derivePresenceKey(name: string, params?: Record<string, unknown> | undefined): string;
710
+ interface UsePresenceResult<TState extends Record<string, unknown>> {
711
+ peers: Array<{
712
+ userId: string;
713
+ state: TState;
714
+ }>;
715
+ set: (state: TState) => Promise<void>;
716
+ }
717
+ interface SyncReactHooks<TQueries extends SyncQueryMap> {
718
+ SyncProvider: (props: SyncProviderProps) => ReactNode2;
719
+ useSync: <K extends keyof TQueries & string>(...args: RequiresParams<TQueries, K> extends true ? [table: K, options: UseSyncOptions<TQueries, K>] : [table: K, options?: UseSyncOptions<TQueries, K>]) => UseSyncResult<TQueries, K>;
720
+ useSyncStatus: () => SyncClientState;
721
+ useRow: <K extends keyof TQueries & string>(table: K, rowId: string) => InferRow<TQueries, K> | null;
722
+ usePendingCount: () => number;
723
+ useEphemeral: <T extends object>(config: {
724
+ key: string;
725
+ userId: string;
726
+ ttlMs?: number;
727
+ }) => {
728
+ events: Record<string, T>;
729
+ broadcast: (data: T) => Promise<void>;
730
+ };
731
+ usePresence: <K extends PresenceKeys<TQueries> & string>(...args: RequiresParams<TQueries, K> extends true ? [name: K, params: InferParams<TQueries, K>] : [name: K]) => UsePresenceResult<InferState<TQueries, K> & Record<string, unknown>>;
732
+ useTotalCount: <K extends keyof TQueries & string>(table: K) => number | null;
733
+ useLoadMore: <K extends keyof TQueries & string>(table: K) => (count: number) => void;
734
+ }
735
+ type UseSyncOptions<
736
+ TQueries extends SyncQueryMap,
737
+ K extends keyof TQueries
738
+ > = RequiresParams<TQueries, K> extends true ? {
739
+ params: InferParams<TQueries, K>;
740
+ includeDeleted?: boolean;
741
+ window?: number;
742
+ } : {
743
+ params?: undefined;
744
+ includeDeleted?: boolean;
745
+ window?: number;
746
+ };
747
+ declare function createSyncReact<TQueries extends SyncQueryMap>(queries?: TQueries): SyncReactHooks<TQueries>;
748
+ export { useTotalCount2 as useTotalCount, useSyncStatus2 as useSyncStatus, useSyncClient, useSync2 as useSync, useRow2 as useRow, usePendingCount2 as usePendingCount, useLoadMore2 as useLoadMore, useEphemeral2 as useEphemeral, derivePresenceKey, createSyncReact, SyncProviderProps, SyncProvider2 as SyncProvider, SyncContext };