@datafn/client 0.0.1 → 0.0.2

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/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { DatafnEvent, DatafnSignal, DatafnSchema, DatafnPlugin, DatafnErrorCode } from '@datafn/core';
1
+ import { DatafnEvent, DatafnSchema, DfqlQueryFragment, DfqlMutationFragment, DfqlTransact, DatafnSignal, DatafnPlugin, SearchProvider, DatafnErrorCode } from '@datafn/core';
2
+ export { KV_RESOURCE_NAME, kvId, ns } from '@datafn/core';
2
3
 
3
4
  /**
4
5
  * Event filtering logic
@@ -12,6 +13,7 @@ interface EventFilter {
12
13
  action?: string | string[];
13
14
  fields?: string | string[];
14
15
  contextKeys?: string[];
16
+ context?: Record<string, unknown>;
15
17
  }
16
18
  /**
17
19
  * Check if an event matches the filter
@@ -23,42 +25,52 @@ declare function matchesFilter(event: DatafnEvent, filter?: EventFilter): boolea
23
25
  */
24
26
 
25
27
  type EventHandler = (event: DatafnEvent) => void;
28
+ interface EventBusOptions {
29
+ /**
30
+ * Error handler called when a subscriber throws an error.
31
+ * Default: logs to console.error
32
+ */
33
+ onError?: (error: unknown, event: DatafnEvent) => void;
34
+ }
26
35
  /**
27
- * Simple in-process event bus
36
+ * Simple in-process event bus with fault-tolerant delivery
28
37
  */
29
38
  declare class EventBus {
30
39
  private subscriptions;
31
40
  private nextId;
41
+ private onError;
42
+ constructor(options?: EventBusOptions);
32
43
  /**
33
44
  * Subscribe to events with optional filtering
34
45
  */
35
46
  subscribe(handler: EventHandler, filter?: EventFilter): () => void;
36
47
  /**
37
- * Emit an event to all matching subscribers
48
+ * Emit an event to all matching subscribers.
49
+ * Errors in handlers are isolated - a throwing handler will not prevent delivery to other handlers.
38
50
  */
39
51
  emit(event: DatafnEvent): void;
40
- }
41
-
42
- /**
43
- * DataFn Table Handle
44
- *
45
- * Represents a table/resource from the schema with methods for query, mutation, signals, and subscriptions.
46
- */
47
-
48
- interface DatafnTable<TRecord = unknown> {
49
- name: string;
50
- version: number;
51
- query(q: unknown): Promise<unknown>;
52
- mutate(m: unknown): Promise<unknown>;
53
- transact(payload: unknown): Promise<unknown>;
54
- signal(q: unknown): DatafnSignal<unknown>;
55
- subscribe(handler: EventHandler, filter?: EventFilter): () => void;
52
+ /**
53
+ * Remove all subscriptions
54
+ */
55
+ clear(): void;
56
56
  }
57
57
 
58
58
  /**
59
59
  * Storage adapter types for local persistence
60
60
  */
61
61
  type DatafnHydrationState = "notStarted" | "hydrating" | "ready";
62
+ /**
63
+ * Factory function for creating storage adapters with namespace-based isolation.
64
+ * Used for multi-user/multi-tenant data isolation.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const storageFactory: DatafnStorageFactory = (namespace) => {
69
+ * return IndexedDbStorageAdapter.createForNamespace("my-app", namespace);
70
+ * };
71
+ * ```
72
+ */
73
+ type DatafnStorageFactory = (namespace: string) => DatafnStorageAdapter;
62
74
  type DatafnChangelogEntry = {
63
75
  /** Monotonic local sequence (assigned by storage adapter). */
64
76
  seq: number;
@@ -66,17 +78,31 @@ type DatafnChangelogEntry = {
66
78
  mutationId: string;
67
79
  mutation: Record<string, unknown>;
68
80
  timestampMs: number;
81
+ /** Actor ID for audit attribution (when available) - AUD-001 */
82
+ actorId?: string;
83
+ /** ISO 8601 timestamp - AUD-001 */
84
+ timestamp?: string;
69
85
  };
70
86
  interface DatafnStorageAdapter {
71
87
  getRecord(resource: string, id: string): Promise<Record<string, unknown> | null>;
72
88
  listRecords(resource: string): Promise<Record<string, unknown>[]>;
73
89
  upsertRecord(resource: string, record: Record<string, unknown>): Promise<void>;
74
90
  deleteRecord(resource: string, id: string): Promise<void>;
91
+ /**
92
+ * Atomic read-modify-write merge. MUST execute in a single transaction.
93
+ * If record doesn't exist, upserts with partial as the full record.
94
+ * Uses one-level-deep merge for object-type fields.
95
+ */
96
+ mergeRecord(resource: string, id: string, partial: Record<string, unknown>): Promise<Record<string, unknown>>;
75
97
  listJoinRows(relationKey: string): Promise<Array<Record<string, unknown>>>;
98
+ getJoinRows(relationKey: string, fromId: string): Promise<Array<Record<string, unknown>>>;
99
+ getJoinRowsInverse(relationKey: string, toId: string): Promise<Array<Record<string, unknown>>>;
76
100
  upsertJoinRow(relationKey: string, row: Record<string, unknown>): Promise<void>;
101
+ setJoinRows(relationKey: string, rows: Array<Record<string, unknown>>): Promise<void>;
77
102
  deleteJoinRow(relationKey: string, from: string, to: string): Promise<void>;
103
+ findRecords(resource: string, field: string, value: unknown): Promise<Record<string, unknown>[]>;
78
104
  getCursor(resource: string): Promise<string | null>;
79
- setCursor(resource: string, cursor: string): Promise<void>;
105
+ setCursor(resource: string, cursor: string | null): Promise<void>;
80
106
  getHydrationState(resource: string): Promise<DatafnHydrationState>;
81
107
  setHydrationState(resource: string, state: DatafnHydrationState): Promise<void>;
82
108
  changelogAppend(entry: Omit<DatafnChangelogEntry, "seq">): Promise<DatafnChangelogEntry>;
@@ -86,13 +112,135 @@ interface DatafnStorageAdapter {
86
112
  changelogAck(options: {
87
113
  throughSeq: number;
88
114
  }): Promise<void>;
115
+ countRecords(resource: string): Promise<number>;
116
+ countJoinRows(relationKey: string): Promise<number>;
117
+ /** Close all connections and release resources. */
118
+ close(): Promise<void>;
119
+ /** Delete all data across all stores. */
120
+ clearAll(): Promise<void>;
121
+ /**
122
+ * Health check: verify core stores exist and are accessible.
123
+ * Returns { ok: true, issues: [] } when healthy.
124
+ * Returns { ok: false, issues: [...] } when corrupted or inaccessible.
125
+ */
126
+ healthCheck(): Promise<{
127
+ ok: boolean;
128
+ issues: string[];
129
+ }>;
130
+ }
131
+
132
+ /**
133
+ * Mutation Execution Utilities
134
+ *
135
+ * Handles mutation execution via remote adapter with event emission.
136
+ */
137
+
138
+ type ShareScope = "record" | "resource";
139
+ type PrincipalShareMutationInput = {
140
+ principalId: string;
141
+ level: string;
142
+ scope?: ShareScope;
143
+ id?: string;
144
+ };
145
+ type PrincipalUnshareMutationInput = {
146
+ principalId: string;
147
+ scope?: ShareScope;
148
+ id?: string;
149
+ };
150
+
151
+ /**
152
+ * Query Execution Utilities
153
+ *
154
+ * Handles query execution via remote adapter or local storage based on hydration state.
155
+ */
156
+
157
+ type PermissionEntry = {
158
+ userId: string;
159
+ level: string;
160
+ grantedBy: string;
161
+ grantedAt: number;
162
+ };
163
+
164
+ /**
165
+ * DataFn Table Handle
166
+ *
167
+ * Represents a table/resource from the schema with methods for query, mutation, signals, and subscriptions.
168
+ */
169
+
170
+ type QueryMetadata = {
171
+ includeTrashed?: boolean;
172
+ includeArchived?: boolean;
173
+ };
174
+ interface DatafnTable<S extends DatafnSchema = DatafnSchema, Name extends string = string, TRecord = unknown> {
175
+ name: Name;
176
+ version: number;
177
+ query(q: DfqlQueryFragment & {
178
+ metadata?: QueryMetadata;
179
+ }): Promise<unknown>;
180
+ mutate(m: DfqlMutationFragment | DfqlMutationFragment[]): Promise<unknown>;
181
+ delete(id: string): Promise<unknown>;
182
+ trash?: (id: string) => Promise<unknown>;
183
+ restore?: (id: string) => Promise<unknown>;
184
+ archive?: (id: string) => Promise<unknown>;
185
+ unarchive?: (id: string) => Promise<unknown>;
186
+ share?: {
187
+ (id: string, userId: string, level: string): Promise<unknown>;
188
+ (input: PrincipalShareMutationInput): Promise<unknown>;
189
+ };
190
+ unshare?: {
191
+ (id: string, userId: string): Promise<unknown>;
192
+ (input: PrincipalUnshareMutationInput): Promise<unknown>;
193
+ };
194
+ getPermissions?: (id: string) => Promise<PermissionEntry[]>;
195
+ transact(payload: DfqlTransact): Promise<unknown>;
196
+ signal(q: DfqlQueryFragment, options?: {
197
+ disableOptimistic?: boolean;
198
+ }): DatafnSignal<unknown>;
199
+ subscribe(handler: EventHandler, filter?: EventFilter): () => void;
89
200
  }
90
201
 
202
+ type CloneUpOptions = {
203
+ resources?: string[];
204
+ includeManyMany?: boolean;
205
+ recordOperation?: "merge" | "replace" | "insert";
206
+ batchSize?: number;
207
+ maxRetries?: number;
208
+ failFast?: boolean;
209
+ clearChangelogOnSuccess?: boolean;
210
+ setGlobalCursorOnSuccess?: boolean;
211
+ pullAfter?: boolean;
212
+ mutationIdPrefix?: string;
213
+ };
214
+ type CloneUpResult = {
215
+ ok: boolean;
216
+ cursor: string;
217
+ stats: {
218
+ resources: Record<string, {
219
+ records: number;
220
+ mutations: number;
221
+ }>;
222
+ joinStores: Record<string, {
223
+ rows: number;
224
+ mutations: number;
225
+ }>;
226
+ batches: number;
227
+ };
228
+ errors: Array<{
229
+ mutationId: string;
230
+ code: string;
231
+ message: string;
232
+ path: string;
233
+ }>;
234
+ /** Optional total count of uploaded records (for event context) */
235
+ uploadedCount?: number;
236
+ };
237
+
91
238
  /**
92
239
  * Sync Facade
93
240
  *
94
241
  * Client-side sync methods that delegate to remote adapter.
95
242
  * When storage is configured, clone/pull results are applied to local storage.
243
+ * Implements HOOK-001 and EVT-003: beforeSync/afterSync hooks and sync lifecycle events
96
244
  */
97
245
 
98
246
  interface SyncFacade {
@@ -100,12 +248,57 @@ interface SyncFacade {
100
248
  clone(payload: unknown): Promise<unknown>;
101
249
  pull(payload: unknown): Promise<unknown>;
102
250
  push(payload: unknown): Promise<unknown>;
251
+ cloneUp(options?: CloneUpOptions): Promise<CloneUpResult>;
252
+ }
253
+
254
+ /**
255
+ * KV API implementation for datafn
256
+ * Provides a first-class key-value store that works with signals and events.
257
+ */
258
+
259
+ interface DatafnKvApi {
260
+ get<T = unknown>(key: string): Promise<T | null>;
261
+ set<T = unknown>(key: string, value: T, params?: {
262
+ context?: Record<string, unknown>;
263
+ debounceMs?: number;
264
+ }): Promise<{
265
+ ok: true;
266
+ key: string;
267
+ } | {
268
+ ok: false;
269
+ error: unknown;
270
+ }>;
271
+ merge(key: string, patch: Record<string, unknown>, params?: {
272
+ context?: Record<string, unknown>;
273
+ debounceMs?: number;
274
+ }): Promise<{
275
+ ok: true;
276
+ key: string;
277
+ } | {
278
+ ok: false;
279
+ error: unknown;
280
+ }>;
281
+ delete(key: string, params?: {
282
+ context?: Record<string, unknown>;
283
+ }): Promise<{
284
+ ok: true;
285
+ key: string;
286
+ } | {
287
+ ok: false;
288
+ error: unknown;
289
+ }>;
290
+ getOrSeed<T = unknown>(key: string, defaults: T): Promise<T>;
291
+ flush(key?: string): Promise<void>;
292
+ signal<T = unknown>(key: string, options?: {
293
+ defaultValue?: T;
294
+ }): DatafnSignal<T>;
103
295
  }
104
296
 
105
297
  /**
106
298
  * DataFn client factory
107
299
  */
108
300
 
301
+ type ResourceNames<S extends DatafnSchema> = S["resources"][number]["name"];
109
302
  interface DatafnRemoteAdapter {
110
303
  query(q: unknown): Promise<unknown>;
111
304
  mutation(m: unknown): Promise<unknown>;
@@ -114,10 +307,132 @@ interface DatafnRemoteAdapter {
114
307
  clone(payload: unknown): Promise<unknown>;
115
308
  pull(payload: unknown): Promise<unknown>;
116
309
  push(payload: unknown): Promise<unknown>;
310
+ reconcile(payload: unknown): Promise<unknown>;
311
+ search?(payload: unknown): Promise<unknown>;
312
+ }
313
+ interface DatafnSyncConfig {
314
+ /**
315
+ * Enable offline support. Requires `storage` adapter.
316
+ */
317
+ offlinability?: boolean;
318
+ /**
319
+ * Remote server URL used by the default HTTP transport and for deriving wsUrl.
320
+ * Optional when `remoteAdapter` is provided.
321
+ */
322
+ remote?: string;
323
+ /**
324
+ * Optional injected adapter used instead of DefaultHttpTransport.
325
+ * Required for extension environments.
326
+ */
327
+ remoteAdapter?: DatafnRemoteAdapter;
328
+ /**
329
+ * Enable WebSocket updates.
330
+ */
331
+ ws?: boolean;
332
+ /**
333
+ * WebSocket URL. If not provided, derived from `remote` when `ws` is enabled.
334
+ */
335
+ wsUrl?: string;
336
+ /**
337
+ * Batch push interval in milliseconds.
338
+ * Must be a positive integer.
339
+ */
340
+ pushInterval?: number;
341
+ /**
342
+ * Batch push page size.
343
+ * Must be a positive integer. Default 100.
344
+ */
345
+ pushBatchSize?: number;
346
+ /**
347
+ * Max retries for push.
348
+ * Must be a non-negative integer. Default 3.
349
+ */
350
+ pushMaxRetries?: number;
351
+ /**
352
+ * Hydration plan for large datasets.
353
+ */
354
+ hydration?: {
355
+ /**
356
+ * Resources that MUST be cloned to `ready` before the app can consider itself hydrated.
357
+ */
358
+ bootResources?: string[];
359
+ /**
360
+ * Resources that MAY hydrate in the background after boot.
361
+ */
362
+ backgroundResources?: string[];
363
+ /**
364
+ * Per-resource clone page size limits used by paginated clone.
365
+ */
366
+ clonePageSize?: number | Record<string, number>;
367
+ };
368
+ /**
369
+ * Explicit mode selection.
370
+ * - "sync": requires remote or remoteAdapter.
371
+ * - "local-only": remote is not required; all local tables start as `ready`.
372
+ */
373
+ mode?: "sync" | "local-only";
374
+ /**
375
+ * WebSocket reconnection configuration with exponential backoff and jitter.
376
+ * Default: enabled with baseDelayMs=1000, multiplier=2, maxDelayMs=60000, jitterMs=500.
377
+ */
378
+ wsReconnect?: {
379
+ enabled?: boolean;
380
+ baseDelayMs?: number;
381
+ maxDelayMs?: number;
382
+ multiplier?: number;
383
+ jitterMs?: number;
384
+ };
385
+ /**
386
+ * Push retry exponential backoff configuration.
387
+ * Default: baseDelayMs=1000, multiplier=2, maxDelayMs=60000, jitterMs=500.
388
+ */
389
+ pushRetryBackoff?: {
390
+ baseDelayMs?: number;
391
+ maxDelayMs?: number;
392
+ multiplier?: number;
393
+ jitterMs?: number;
394
+ };
395
+ /**
396
+ * Push interval exponential backoff configuration when push keeps failing.
397
+ * When a push round exhausts all retries and fails, the next round is delayed
398
+ * using exponential backoff: pushInterval * multiplier^consecutiveFailures.
399
+ * On first success after failures, backoff is reset to configured pushInterval.
400
+ * Default: baseMultiplier=2, maxDelayMs=300000 (5 minutes), jitterMs=1000.
401
+ */
402
+ pushIntervalBackoff?: {
403
+ baseMultiplier?: number;
404
+ maxDelayMs?: number;
405
+ jitterMs?: number;
406
+ };
407
+ /**
408
+ * Pull batch size limit per request.
409
+ * Must be a positive integer between 10 and 10000.
410
+ * Default: 200.
411
+ */
412
+ pullBatchSize?: number;
413
+ /**
414
+ * Maximum number of catch-up pull iterations per sync cycle (CLIENT-PULL-003).
415
+ * Prevents infinite loops if the server continuously returns hasMore=true.
416
+ * Default: 50.
417
+ */
418
+ maxPullIterations?: number;
419
+ /**
420
+ * Enable cross-tab coordination via BroadcastChannel.
421
+ * When enabled, mutation events are relayed to other same-origin tabs for near-instant reactivity.
422
+ * Default: false (opt-in).
423
+ */
424
+ crossTab?: boolean;
425
+ /**
426
+ * Defer search indexing for initial clone and rebuild asynchronously after clone completion.
427
+ */
428
+ skipCloneIndexing?: boolean;
117
429
  }
118
- interface DatafnClientConfig {
119
- schema: DatafnSchema;
120
- remote: DatafnRemoteAdapter;
430
+ interface DatafnClientConfig<S extends DatafnSchema> {
431
+ schema: S;
432
+ /**
433
+ * Sync configuration.
434
+ */
435
+ sync?: DatafnSyncConfig;
121
436
  /**
122
437
  * Optional plugins for client-side hook execution
123
438
  */
@@ -126,25 +441,114 @@ interface DatafnClientConfig {
126
441
  * Stable client/device identifier used for idempotency and offline change logs.
127
442
  * Required when `storage` is provided.
128
443
  */
129
- clientId?: string;
444
+ clientId: string;
445
+ /**
446
+ * Local persistence adapter. Can be:
447
+ * - A direct `DatafnStorageAdapter` instance
448
+ * - A factory function `(namespace: string) => DatafnStorageAdapter` for multi-user isolation
449
+ *
450
+ * When using a factory function, `namespace` must also be provided.
451
+ */
452
+ storage?: DatafnStorageAdapter | DatafnStorageFactory;
130
453
  /**
131
- * Local persistence adapter. When provided, sync results are applied to local storage.
454
+ * Namespace for client-side data isolation.
455
+ * Used to construct isolated storage (IndexedDB database names, BroadcastChannel names).
456
+ * Required when `storage` is a factory function.
132
457
  */
133
- storage?: DatafnStorageAdapter;
458
+ namespace?: string;
134
459
  getTimestamp?: () => number;
460
+ /**
461
+ * Resource-aware ID generator function for insert operations.
462
+ * If not provided, defaults to crypto.randomUUID() with resource prefix.
463
+ * Allows users to use custom ID strategies (UUID v7, ULID, etc.)
464
+ * without adding dependencies to @datafn/client.
465
+ */
466
+ generateId?: (params: {
467
+ resource: string;
468
+ idPrefix?: string;
469
+ }) => string;
470
+ /**
471
+ * Optional search provider for client-side search query routing.
472
+ */
473
+ searchProvider?: SearchProvider;
135
474
  }
136
- interface DatafnClient {
137
- table<TRecord = unknown>(name: string): DatafnTable<TRecord>;
475
+ /**
476
+ * Override options for switchContext.
477
+ * Only the provided fields are changed; everything else is inherited from the current config.
478
+ */
479
+ type SwitchContextOverride = {
480
+ /** New namespace (replaces current) */
481
+ namespace?: string;
482
+ /** New sync configuration (replaces current, not merged) */
483
+ sync?: DatafnSyncConfig;
484
+ /** New storage adapter or factory (replaces current) */
485
+ storage?: DatafnStorageAdapter | DatafnStorageFactory;
486
+ };
487
+ type DatafnClient<S extends DatafnSchema> = {
488
+ table<Name extends ResourceNames<S>>(name: Name): DatafnTable<S, Name>;
138
489
  query(q: unknown | unknown[]): Promise<unknown>;
139
490
  mutate(mutation: unknown | unknown[]): Promise<unknown>;
140
491
  transact(payload: unknown): Promise<unknown>;
141
492
  subscribe(handler: EventHandler, filter?: EventFilter): () => void;
142
- sync: SyncFacade;
143
- }
493
+ sync: SyncFacade & {
494
+ start(): Promise<void>;
495
+ stop(): void;
496
+ pullNow(): Promise<void>;
497
+ cloneNow(): Promise<void>;
498
+ reconcileNow(): Promise<void>;
499
+ };
500
+ kv: DatafnKvApi;
501
+ /** Tear down client: stop sync, close connections, unsubscribe all, release resources. */
502
+ destroy(): Promise<void>;
503
+ /** Wipe all local data (IndexedDB stores, cursors, changelog, hydration state). */
504
+ clear(): Promise<void>;
505
+ /** Flush a specific debounced mutation immediately. */
506
+ flush(key?: string): Promise<void>;
507
+ /** Flush all pending debounced mutations immediately. */
508
+ flushAll(): Promise<void>;
509
+ /** Export all local records as structured JSON. */
510
+ exportData(options?: {
511
+ resources?: string[];
512
+ }): Promise<unknown>;
513
+ /** Import records from a structured JSON payload. */
514
+ importData(data: unknown, options?: {
515
+ triggerCloneUp?: boolean;
516
+ }): Promise<unknown>;
517
+ /** Check storage health and verify hydration state consistency. */
518
+ checkHealth(): Promise<{
519
+ ok: boolean;
520
+ issues: string[];
521
+ action?: "none" | "reclone";
522
+ }>;
523
+ /** Perform a cross-resource search using the configured search provider. */
524
+ search(params: unknown): Promise<unknown>;
525
+ /**
526
+ * Switch to a different configuration context (auth, sync mode, or storage).
527
+ * Destroys the current underlying client and recreates it with merged config.
528
+ * Concurrent calls are serialized. Auto-starts sync when sync.mode is "sync".
529
+ */
530
+ switchContext(override: SwitchContextOverride): Promise<void>;
531
+ /** Get the current resolved namespace, or undefined if not configured. */
532
+ currentNamespace(): string | undefined;
533
+ /**
534
+ * Subscribe to client lifecycle changes.
535
+ * The callback fires immediately with the stable proxy reference,
536
+ * and again after every switchContext() completes.
537
+ * Returns an unsubscribe function.
538
+ */
539
+ subscribeClient(fn: (client: DatafnClient<S>) => void): () => void;
540
+ } & {
541
+ [Name in ResourceNames<S>]: DatafnTable<S, Name>;
542
+ };
144
543
  /**
145
- * Create a DataFn client
544
+ * Create a DataFn client with built-in context switching.
545
+ *
546
+ * The returned client is a stable Proxy reference that always delegates to the
547
+ * current underlying client. Use `switchContext()` to switch auth, sync mode,
548
+ * or storage without replacing the reference. Use `subscribeClient()` to react
549
+ * to switches (e.g. to recreate signals in framework adapters).
146
550
  */
147
- declare function createDatafnClient(config: DatafnClientConfig): DatafnClient;
551
+ declare function createDatafnClient<S extends DatafnSchema>(config: DatafnClientConfig<S>): DatafnClient<S>;
148
552
 
149
553
  /**
150
554
  * DataFn Client Error Types
@@ -159,12 +563,16 @@ type DatafnClientError = {
159
563
  };
160
564
  };
161
565
  /**
162
- * Create a DataFnClientError and throw it
566
+ * Build and throw a DatafnClientError.
163
567
  */
164
- declare function createClientError(code: DatafnClientError["code"], message: string, details: {
568
+ declare function throwClientError(code: DatafnClientError["code"], message: string, details: {
165
569
  path: string;
166
570
  [key: string]: unknown;
167
571
  }): never;
572
+ /**
573
+ * Alias for `throwClientError`.
574
+ */
575
+ declare const createClientError: typeof throwClientError;
168
576
 
169
577
  /**
170
578
  * Remote Response Unwrapping Utilities
@@ -194,16 +602,28 @@ declare class MemoryStorageAdapter implements DatafnStorageAdapter {
194
602
  private hydration;
195
603
  private changelog;
196
604
  private changelogSeq;
197
- constructor();
605
+ private changelogIndex;
606
+ private validResources?;
607
+ constructor(resources?: string[]);
608
+ private validateTableName;
198
609
  getRecord(resource: string, id: string): Promise<Record<string, unknown> | null>;
199
610
  listRecords(resource: string): Promise<Record<string, unknown>[]>;
200
611
  upsertRecord(resource: string, record: Record<string, unknown>): Promise<void>;
201
612
  deleteRecord(resource: string, id: string): Promise<void>;
613
+ /**
614
+ * Atomic read-modify-write merge using one-level-deep merge.
615
+ */
616
+ mergeRecord(resource: string, id: string, partial: Record<string, unknown>): Promise<Record<string, unknown>>;
617
+ private deepMergeOneLevel;
202
618
  listJoinRows(relationKey: string): Promise<Array<Record<string, unknown>>>;
619
+ getJoinRows(relationKey: string, fromId: string): Promise<Array<Record<string, unknown>>>;
620
+ getJoinRowsInverse(relationKey: string, toId: string): Promise<Array<Record<string, unknown>>>;
203
621
  upsertJoinRow(relationKey: string, row: Record<string, unknown>): Promise<void>;
622
+ setJoinRows(relationKey: string, rows: Array<Record<string, unknown>>): Promise<void>;
204
623
  deleteJoinRow(relationKey: string, from: string, to: string): Promise<void>;
624
+ findRecords(resource: string, field: string, value: unknown): Promise<Record<string, unknown>[]>;
205
625
  getCursor(resource: string): Promise<string | null>;
206
- setCursor(resource: string, cursor: string): Promise<void>;
626
+ setCursor(resource: string, cursor: string | null): Promise<void>;
207
627
  getHydrationState(resource: string): Promise<DatafnHydrationState>;
208
628
  setHydrationState(resource: string, state: DatafnHydrationState): Promise<void>;
209
629
  changelogAppend(entry: Omit<DatafnChangelogEntry, "seq">): Promise<DatafnChangelogEntry>;
@@ -213,7 +633,14 @@ declare class MemoryStorageAdapter implements DatafnStorageAdapter {
213
633
  changelogAck(options: {
214
634
  throughSeq: number;
215
635
  }): Promise<void>;
216
- clear(): void;
636
+ countRecords(resource: string): Promise<number>;
637
+ countJoinRows(relationKey: string): Promise<number>;
638
+ close(): Promise<void>;
639
+ clearAll(): Promise<void>;
640
+ healthCheck(): Promise<{
641
+ ok: boolean;
642
+ issues: string[];
643
+ }>;
217
644
  }
218
645
 
219
646
  /**
@@ -223,17 +650,76 @@ declare class MemoryStorageAdapter implements DatafnStorageAdapter {
223
650
 
224
651
  declare class IndexedDbStorageAdapter implements DatafnStorageAdapter {
225
652
  private dbPromise;
226
- constructor(dbName?: string);
653
+ private validResources?;
654
+ private schema?;
655
+ readonly dbName: string;
656
+ /**
657
+ * Create an IndexedDB storage adapter with schema-aware store creation.
658
+ * This is the **recommended** way to create the adapter — schema is required,
659
+ * ensuring all resource object stores and join tables are created in IndexedDB.
660
+ *
661
+ * If the database already exists but is missing stores (stale DB), the adapter
662
+ * automatically bumps the version and creates the missing stores.
663
+ *
664
+ * @param options.dbName - Database name (e.g., "my-app-db")
665
+ * @param options.schema - DataFn schema (required — used to create object stores)
666
+ *
667
+ * @example
668
+ * ```typescript
669
+ * const storage = IndexedDbStorageAdapter.create({
670
+ * dbName: "my-app-db",
671
+ * schema,
672
+ * });
673
+ * ```
674
+ */
675
+ static create(options: {
676
+ dbName: string;
677
+ schema: DatafnSchema;
678
+ }): IndexedDbStorageAdapter;
679
+ /**
680
+ * Create a storage adapter with a namespace-derived database name.
681
+ * The namespace string has `:` replaced with `_` for the IDB database name.
682
+ *
683
+ * @example
684
+ * ```typescript
685
+ * const storage = IndexedDbStorageAdapter.createForNamespace("my-app", "org:user-1");
686
+ * // IDB database name: "my-app_org_user-1"
687
+ * ```
688
+ */
689
+ static createForNamespace(baseDbName: string, namespace: string, resources?: string[], schema?: DatafnSchema): IndexedDbStorageAdapter;
690
+ constructor(dbName?: string, resources?: string[], schema?: DatafnSchema);
691
+ /**
692
+ * Open (or upgrade) the IndexedDB database at the given version.
693
+ * All object store creation happens inside `onupgradeneeded`.
694
+ */
695
+ private openDatabase;
696
+ /**
697
+ * Verify all expected stores exist after initial open.
698
+ * If stores are missing (stale DB opened without schema previously),
699
+ * close the database and reopen with a bumped version to trigger
700
+ * `onupgradeneeded`, which will create the missing stores.
701
+ */
702
+ private ensureStores;
703
+ private validateTableName;
227
704
  private getStore;
228
705
  getRecord(resource: string, id: string): Promise<Record<string, unknown> | null>;
229
706
  listRecords(resource: string): Promise<Record<string, unknown>[]>;
230
707
  upsertRecord(resource: string, record: Record<string, unknown>): Promise<void>;
231
708
  deleteRecord(resource: string, id: string): Promise<void>;
232
- listJoinRows(relationKey: string): Promise<Array<Record<string, unknown>>>;
233
- upsertJoinRow(relationKey: string, row: Record<string, unknown>): Promise<void>;
234
- deleteJoinRow(relationKey: string, from: string, to: string): Promise<void>;
709
+ /**
710
+ * Atomic read-modify-write merge using a single transaction.
711
+ * Uses one-level-deep merge for object-type fields.
712
+ */
713
+ mergeRecord(resource: string, id: string, partial: Record<string, unknown>): Promise<Record<string, unknown>>;
714
+ listJoinRows(storeName: string): Promise<Array<Record<string, unknown>>>;
715
+ getJoinRows(storeName: string, fromId: string): Promise<Array<Record<string, unknown>>>;
716
+ getJoinRowsInverse(storeName: string, toId: string): Promise<Array<Record<string, unknown>>>;
717
+ upsertJoinRow(storeName: string, row: Record<string, unknown>): Promise<void>;
718
+ setJoinRows(storeName: string, rows: Array<Record<string, unknown>>): Promise<void>;
719
+ deleteJoinRow(storeName: string, from: string, to: string): Promise<void>;
720
+ findRecords(resource: string, field: string, value: unknown): Promise<Record<string, unknown>[]>;
235
721
  getCursor(resource: string): Promise<string | null>;
236
- setCursor(resource: string, cursor: string): Promise<void>;
722
+ setCursor(resource: string, cursor: string | null): Promise<void>;
237
723
  getHydrationState(resource: string): Promise<DatafnHydrationState>;
238
724
  setHydrationState(resource: string, state: DatafnHydrationState): Promise<void>;
239
725
  changelogAppend(entry: Omit<DatafnChangelogEntry, "seq">): Promise<DatafnChangelogEntry>;
@@ -243,6 +729,54 @@ declare class IndexedDbStorageAdapter implements DatafnStorageAdapter {
243
729
  changelogAck(options: {
244
730
  throughSeq: number;
245
731
  }): Promise<void>;
732
+ countRecords(resource: string): Promise<number>;
733
+ countJoinRows(relationKey: string): Promise<number>;
734
+ close(): Promise<void>;
735
+ clearAll(): Promise<void>;
736
+ healthCheck(): Promise<{
737
+ ok: boolean;
738
+ issues: string[];
739
+ }>;
246
740
  }
247
741
 
248
- export { type DatafnChangelogEntry, type DatafnClient, type DatafnClientConfig, type DatafnClientError, type DatafnHydrationState, type DatafnRemoteAdapter, type DatafnStorageAdapter, type DatafnTable, EventBus, type EventFilter, type EventHandler, IndexedDbStorageAdapter, MemoryStorageAdapter, createClientError, createDatafnClient, matchesFilter, unwrapRemoteSuccess };
742
+ /**
743
+ * Date Codec (CODEC-001)
744
+ *
745
+ * Provides schema-driven date serialization and parsing:
746
+ * - Outbound (mutation): Date objects → epoch milliseconds (number)
747
+ * - Inbound (query result): epoch milliseconds or ISO strings → Date objects for schema date fields
748
+ * - Deterministic error behavior for invalid dates
749
+ */
750
+
751
+ /**
752
+ * Serialize Date fields in mutation records to epoch milliseconds (CODEC-001 outbound)
753
+ *
754
+ * @param schema Schema definition
755
+ * @param resource Resource name
756
+ * @param record Record to serialize
757
+ * @returns Serialized record with Date → epoch milliseconds (number)
758
+ * @throws DFQL_INVALID if a date field contains a non-Date value
759
+ */
760
+ declare function serializeDateFields(schema: DatafnSchema, resource: string, record: Record<string, unknown>): Record<string, unknown>;
761
+ /**
762
+ * Parse date fields in query results from epoch milliseconds or ISO strings to Date objects (CODEC-001 inbound)
763
+ *
764
+ * @param schema Schema definition
765
+ * @param resource Resource name
766
+ * @param record Record to parse
767
+ * @returns Parsed record with epoch milliseconds or ISO string → Date
768
+ * @throws DFQL_INVALID if a date field contains an invalid value
769
+ */
770
+ declare function parseDateFields(schema: DatafnSchema, resource: string, record: Record<string, unknown>): Record<string, unknown>;
771
+ /**
772
+ * Parse date fields in a query result payload
773
+ * Handles both non-aggregate (data array) and aggregate (groups array) results
774
+ *
775
+ * @param schema Schema definition
776
+ * @param resource Resource name
777
+ * @param result Query result payload
778
+ * @returns Result with parsed Date fields
779
+ */
780
+ declare function parseQueryResultDates(schema: DatafnSchema, resource: string, result: any): any;
781
+
782
+ export { type CloneUpOptions, type CloneUpResult, type DatafnChangelogEntry, type DatafnClient, type DatafnClientConfig, type DatafnClientError, type DatafnHydrationState, type DatafnKvApi, type DatafnRemoteAdapter, type DatafnStorageAdapter, type DatafnStorageFactory, type DatafnTable, EventBus, type EventFilter, type EventHandler, IndexedDbStorageAdapter, MemoryStorageAdapter, type PermissionEntry, type SwitchContextOverride, createClientError, createDatafnClient, matchesFilter, parseDateFields, parseQueryResultDates, serializeDateFields, throwClientError, unwrapRemoteSuccess };