@anfenn/dync 1.1.3 → 1.2.1

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 (39) hide show
  1. package/README.md +4 -3
  2. package/dist/capacitor.d.cts +1 -1
  3. package/dist/capacitor.d.ts +1 -1
  4. package/dist/{dexie-B3Ihrrxi.d.ts → dexie-Cxn4kUoF.d.ts} +1 -1
  5. package/dist/{dexie-BRWUYM02.d.cts → dexie-D8u9cGSy.d.cts} +1 -1
  6. package/dist/dexie.cjs +11 -0
  7. package/dist/dexie.cjs.map +1 -1
  8. package/dist/dexie.d.cts +2 -2
  9. package/dist/dexie.d.ts +2 -2
  10. package/dist/dexie.js +11 -0
  11. package/dist/dexie.js.map +1 -1
  12. package/dist/expoSqlite.d.cts +1 -1
  13. package/dist/expoSqlite.d.ts +1 -1
  14. package/dist/index.cjs +188 -8
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +6 -6
  17. package/dist/index.d.ts +6 -6
  18. package/dist/index.js +188 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/node.d.cts +1 -1
  21. package/dist/node.d.ts +1 -1
  22. package/dist/react/index.d.cts +3 -3
  23. package/dist/react/index.d.ts +3 -3
  24. package/dist/{types-DhlgTu1o.d.ts → types-Da3ZhO9Y.d.cts} +3 -3
  25. package/dist/{types-6-NyRQ0D.d.cts → types-DcEg2pvl.d.cts} +1 -1
  26. package/dist/{types-6-NyRQ0D.d.ts → types-DcEg2pvl.d.ts} +1 -1
  27. package/dist/{types-Di82FTAL.d.cts → types-DnHiaBZV.d.ts} +3 -3
  28. package/dist/wa-sqlite.d.cts +1 -1
  29. package/dist/wa-sqlite.d.ts +1 -1
  30. package/package.json +2 -2
  31. package/src/core/SyncAwareCollection.ts +124 -0
  32. package/src/core/SyncAwareWhereClause.ts +66 -0
  33. package/src/core/pushOperations.ts +14 -7
  34. package/src/core/tableEnhancers.ts +18 -0
  35. package/src/index.shared.ts +7 -7
  36. package/src/storage/dexie/DexieAdapter.ts +13 -2
  37. package/src/storage/sqlite/SQLiteAdapter.ts +1 -5
  38. package/src/storage/sqlite/types.ts +1 -1
  39. package/src/types.ts +9 -3
package/dist/node.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as better_sqlite3 from 'better-sqlite3';
2
- import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-6-NyRQ0D.cjs';
2
+ import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-DcEg2pvl.cjs';
3
3
 
4
4
  /**
5
5
  * Options for configuring the BetterSQLite3Driver.
package/dist/node.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as better_sqlite3 from 'better-sqlite3';
2
- import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-6-NyRQ0D.js';
2
+ import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-DcEg2pvl.js';
3
3
 
4
4
  /**
5
5
  * Options for configuring the BetterSQLite3Driver.
@@ -1,7 +1,7 @@
1
- import { a as SyncApi, k as SyncState } from '../types-Di82FTAL.cjs';
2
- import '../dexie-BRWUYM02.cjs';
1
+ import { a as SyncApi, k as SyncState } from '../types-Da3ZhO9Y.cjs';
2
+ import '../dexie-D8u9cGSy.cjs';
3
3
  import 'dexie';
4
- import '../types-6-NyRQ0D.cjs';
4
+ import '../types-DcEg2pvl.cjs';
5
5
 
6
6
  /** Minimal database interface for React hooks */
7
7
  interface DyncLike {
@@ -1,7 +1,7 @@
1
- import { a as SyncApi, k as SyncState } from '../types-DhlgTu1o.js';
2
- import '../dexie-B3Ihrrxi.js';
1
+ import { a as SyncApi, k as SyncState } from '../types-DnHiaBZV.js';
2
+ import '../dexie-Cxn4kUoF.js';
3
3
  import 'dexie';
4
- import '../types-6-NyRQ0D.js';
4
+ import '../types-DcEg2pvl.js';
5
5
 
6
6
  /** Minimal database interface for React hooks */
7
7
  interface DyncLike {
@@ -1,4 +1,4 @@
1
- import { d as StorageAdapter, a as StorageTable } from './dexie-B3Ihrrxi.js';
1
+ import { d as StorageAdapter, a as StorageTable } from './dexie-D8u9cGSy.cjs';
2
2
 
3
3
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
4
4
  interface Logger {
@@ -23,7 +23,7 @@ interface CrudSyncApi {
23
23
  add: (item: any) => Promise<any | undefined>;
24
24
  update: (id: any, changes: any, item: any) => Promise<boolean>;
25
25
  remove: (id: any) => Promise<void>;
26
- list: (lastUpdatedAt: Date) => Promise<any[]>;
26
+ list: (newestUpdatedAt: Date) => Promise<any[]>;
27
27
  listExtraIntervalMs?: number;
28
28
  firstLoad?: (lastId: any) => Promise<any[]>;
29
29
  }
@@ -117,7 +117,7 @@ type SyncApi = {
117
117
  onMutation: (fn: (event: MutationEvent) => void) => () => void;
118
118
  };
119
119
  interface MutationEvent {
120
- type: 'add' | 'update' | 'delete' | 'bulkAdd' | 'bulkPut' | 'bulkDelete' | 'clear' | 'put' | 'modify' | 'pull';
120
+ type: 'add' | 'update' | 'delete' | 'pull';
121
121
  tableName: string;
122
122
  keys?: unknown[];
123
123
  }
@@ -1,5 +1,5 @@
1
1
  interface SQLiteAdapterOptions {
2
- debug?: boolean | ((statement: string, parameters?: any[]) => void);
2
+ debug?: boolean;
3
3
  }
4
4
  interface SQLiteRunResult {
5
5
  changes?: number;
@@ -1,5 +1,5 @@
1
1
  interface SQLiteAdapterOptions {
2
- debug?: boolean | ((statement: string, parameters?: any[]) => void);
2
+ debug?: boolean;
3
3
  }
4
4
  interface SQLiteRunResult {
5
5
  changes?: number;
@@ -1,4 +1,4 @@
1
- import { d as StorageAdapter, a as StorageTable } from './dexie-BRWUYM02.cjs';
1
+ import { d as StorageAdapter, a as StorageTable } from './dexie-Cxn4kUoF.js';
2
2
 
3
3
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
4
4
  interface Logger {
@@ -23,7 +23,7 @@ interface CrudSyncApi {
23
23
  add: (item: any) => Promise<any | undefined>;
24
24
  update: (id: any, changes: any, item: any) => Promise<boolean>;
25
25
  remove: (id: any) => Promise<void>;
26
- list: (lastUpdatedAt: Date) => Promise<any[]>;
26
+ list: (newestUpdatedAt: Date) => Promise<any[]>;
27
27
  listExtraIntervalMs?: number;
28
28
  firstLoad?: (lastId: any) => Promise<any[]>;
29
29
  }
@@ -117,7 +117,7 @@ type SyncApi = {
117
117
  onMutation: (fn: (event: MutationEvent) => void) => () => void;
118
118
  };
119
119
  interface MutationEvent {
120
- type: 'add' | 'update' | 'delete' | 'bulkAdd' | 'bulkPut' | 'bulkDelete' | 'clear' | 'put' | 'modify' | 'pull';
120
+ type: 'add' | 'update' | 'delete' | 'pull';
121
121
  tableName: string;
122
122
  keys?: unknown[];
123
123
  }
@@ -1,4 +1,4 @@
1
- import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-6-NyRQ0D.cjs';
1
+ import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-DcEg2pvl.cjs';
2
2
 
3
3
  /**
4
4
  * Virtual File System (VFS) options for wa-sqlite.
@@ -1,4 +1,4 @@
1
- import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-6-NyRQ0D.js';
1
+ import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-DcEg2pvl.js';
2
2
 
3
3
  /**
4
4
  * Virtual File System (VFS) options for wa-sqlite.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anfenn/dync",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "private": false,
5
5
  "description": "Write once, run IndexedDB & SQLite with sync anywhere - React, React Native, Expo, Capacitor, Electron & Node.js",
6
6
  "keywords": [
@@ -144,7 +144,7 @@
144
144
  "test:browser:full": "pnpm --filter tests test:browser:full",
145
145
  "reinstall": "find . -name node_modules -type d -prune -exec rm -rf {} + && pnpm install",
146
146
  "smoke": "pnpm build:all && pnpm test",
147
- "hascommits": "[ -n \"$(git log @{u}.. --oneline)\" ] || { echo \"No unpushed commits, aborting.\"; exit 1; }",
147
+ "hascommits": "[ -n \"$(git log @{u}.. --oneline)\" ] || { echo \"No un-pushed commits, aborting.\"; exit 1; }",
148
148
  "prepush": "pnpm hascommits && pnpm build:all && pnpm test:all && pnpm test:browser:full",
149
149
  "release": "bash -c 'pnpm prepush && { [ \"$1\" = \"--no-patch\" ] || npm version patch --no-tag --force; } && git push && npm login && npm publish' --"
150
150
  },
@@ -0,0 +1,124 @@
1
+ import type { StorageCollection, StorageTable, StorageWhereClause } from '../storage/types';
2
+ import { SyncAwareWhereClause } from './SyncAwareWhereClause';
3
+
4
+ /**
5
+ * Wraps a StorageCollection so that modify() and delete() create pending sync
6
+ * changes by delegating to the already-wrapped table.bulkUpdate / bulkDelete.
7
+ * All read and chaining operations delegate transparently to the inner collection.
8
+ */
9
+ export class SyncAwareCollection<T> implements StorageCollection<T> {
10
+ constructor(
11
+ private readonly inner: StorageCollection<T>,
12
+ private readonly tableRef: StorageTable<any>,
13
+ ) {}
14
+
15
+ private wrap(col: StorageCollection<T>): SyncAwareCollection<T> {
16
+ return new SyncAwareCollection(col, this.tableRef);
17
+ }
18
+
19
+ // ---- read-only / pass-through ----
20
+ first() {
21
+ return this.inner.first();
22
+ }
23
+ last() {
24
+ return this.inner.last();
25
+ }
26
+ each(cb: (item: T, idx: number) => void | Promise<void>) {
27
+ return this.inner.each(cb);
28
+ }
29
+ eachKey(cb: (key: unknown, idx: number) => void | Promise<void>) {
30
+ return this.inner.eachKey(cb);
31
+ }
32
+ eachPrimaryKey(cb: (key: unknown, idx: number) => void | Promise<void>) {
33
+ return this.inner.eachPrimaryKey(cb);
34
+ }
35
+ eachUniqueKey(cb: (key: unknown, idx: number) => void | Promise<void>) {
36
+ return this.inner.eachUniqueKey(cb);
37
+ }
38
+ keys() {
39
+ return this.inner.keys();
40
+ }
41
+ primaryKeys() {
42
+ return this.inner.primaryKeys();
43
+ }
44
+ uniqueKeys() {
45
+ return this.inner.uniqueKeys();
46
+ }
47
+ count() {
48
+ return this.inner.count();
49
+ }
50
+ sortBy(key: string) {
51
+ return this.inner.sortBy(key);
52
+ }
53
+ toArray() {
54
+ return this.inner.toArray();
55
+ }
56
+
57
+ // ---- chaining — preserve sync-awareness ----
58
+ distinct() {
59
+ return this.wrap(this.inner.distinct());
60
+ }
61
+ clone(props?: Record<string, unknown>) {
62
+ return this.wrap(this.inner.clone(props));
63
+ }
64
+ reverse() {
65
+ return this.wrap(this.inner.reverse());
66
+ }
67
+ offset(n: number) {
68
+ return this.wrap(this.inner.offset(n));
69
+ }
70
+ limit(n: number) {
71
+ return this.wrap(this.inner.limit(n));
72
+ }
73
+ toCollection() {
74
+ return this.wrap(this.inner.toCollection());
75
+ }
76
+ jsFilter(predicate: (item: T) => boolean) {
77
+ return this.wrap(this.inner.jsFilter(predicate));
78
+ }
79
+ or(index: string): StorageWhereClause<T> {
80
+ return new SyncAwareWhereClause(this.inner.or(index), this.tableRef);
81
+ }
82
+
83
+ // ---- sync-aware mutations ----
84
+
85
+ async delete(): Promise<number> {
86
+ const records = await this.inner.toArray();
87
+ if (records.length === 0) return 0;
88
+ const keys = records.map((r: any) => r._localId as string);
89
+ await this.tableRef.bulkDelete(keys);
90
+ return records.length;
91
+ }
92
+
93
+ async modify(changes: Partial<T> | ((item: T) => void | Promise<void>)): Promise<number> {
94
+ const records = await this.inner.toArray();
95
+ if (records.length === 0) return 0;
96
+
97
+ if (typeof changes === 'function') {
98
+ const keysAndChanges: Array<{ key: string; changes: Partial<T> }> = [];
99
+ for (const record of records) {
100
+ const draft = { ...record } as T;
101
+ await (changes as (item: T) => void | Promise<void>)(draft);
102
+ // Compute shallow delta
103
+ const delta: Partial<T> = {};
104
+ const allKeys = new Set([...Object.keys(record as object), ...Object.keys(draft as object)]) as Set<keyof T>;
105
+ for (const key of allKeys) {
106
+ if ((draft as any)[key] !== (record as any)[key]) {
107
+ (delta as any)[key] = (draft as any)[key];
108
+ }
109
+ }
110
+ if (Object.keys(delta).length > 0) {
111
+ keysAndChanges.push({ key: (record as any)._localId, changes: delta });
112
+ }
113
+ }
114
+ if (keysAndChanges.length > 0) {
115
+ await this.tableRef.bulkUpdate(keysAndChanges as any);
116
+ }
117
+ } else {
118
+ const keysAndChanges = records.map((r: any) => ({ key: r._localId as string, changes }));
119
+ await this.tableRef.bulkUpdate(keysAndChanges as any);
120
+ }
121
+
122
+ return records.length;
123
+ }
124
+ }
@@ -0,0 +1,66 @@
1
+ import type { StorageCollection, StorageTable, StorageWhereClause } from '../storage/types';
2
+ import { SyncAwareCollection } from './SyncAwareCollection';
3
+
4
+ /**
5
+ * Wraps a StorageWhereClause so that every collection it produces is a
6
+ * SyncAwareCollection.
7
+ */
8
+ export class SyncAwareWhereClause<T> implements StorageWhereClause<T> {
9
+ constructor(
10
+ private readonly inner: StorageWhereClause<T>,
11
+ private readonly tableRef: StorageTable<any>,
12
+ ) {}
13
+
14
+ private wrap(col: StorageCollection<T>): SyncAwareCollection<T> {
15
+ return new SyncAwareCollection(col, this.tableRef);
16
+ }
17
+
18
+ equals(value: any) {
19
+ return this.wrap(this.inner.equals(value));
20
+ }
21
+ above(value: any) {
22
+ return this.wrap(this.inner.above(value));
23
+ }
24
+ aboveOrEqual(value: any) {
25
+ return this.wrap(this.inner.aboveOrEqual(value));
26
+ }
27
+ below(value: any) {
28
+ return this.wrap(this.inner.below(value));
29
+ }
30
+ belowOrEqual(value: any) {
31
+ return this.wrap(this.inner.belowOrEqual(value));
32
+ }
33
+ between(lower: any, upper: any, includeLower?: boolean, includeUpper?: boolean) {
34
+ return this.wrap(this.inner.between(lower, upper, includeLower, includeUpper));
35
+ }
36
+ inAnyRange(ranges: Array<[any, any]>, options?: { includeLower?: boolean; includeUpper?: boolean }) {
37
+ return this.wrap(this.inner.inAnyRange(ranges, options));
38
+ }
39
+ startsWith(prefix: string) {
40
+ return this.wrap(this.inner.startsWith(prefix));
41
+ }
42
+ startsWithIgnoreCase(prefix: string) {
43
+ return this.wrap(this.inner.startsWithIgnoreCase(prefix));
44
+ }
45
+ startsWithAnyOf(...args: any[]) {
46
+ return this.wrap((this.inner.startsWithAnyOf as any)(...args));
47
+ }
48
+ startsWithAnyOfIgnoreCase(...args: any[]) {
49
+ return this.wrap((this.inner.startsWithAnyOfIgnoreCase as any)(...args));
50
+ }
51
+ equalsIgnoreCase(value: string) {
52
+ return this.wrap(this.inner.equalsIgnoreCase(value));
53
+ }
54
+ anyOf(...args: any[]) {
55
+ return this.wrap((this.inner.anyOf as any)(...args));
56
+ }
57
+ anyOfIgnoreCase(...args: any[]) {
58
+ return this.wrap((this.inner.anyOfIgnoreCase as any)(...args));
59
+ }
60
+ noneOf(...args: any[]) {
61
+ return this.wrap((this.inner.noneOf as any)(...args));
62
+ }
63
+ notEqual(value: any) {
64
+ return this.wrap(this.inner.notEqual(value));
65
+ }
66
+ }
@@ -1,6 +1,6 @@
1
1
  import { createLocalId, orderFor } from '../helpers';
2
2
  import type { Logger } from '../logger';
3
- import type { CrudSyncApi, BatchPushPayload, BatchPushResult, BatchSync, PendingChange, SyncOptions } from '../types';
3
+ import type { CrudSyncApi, BatchPushPayload, BatchPushResult, BatchSync, PendingChange, ResolvedSyncOptions } from '../types';
4
4
  import { SyncAction } from '../types';
5
5
  import type { StorageTable } from '../storage/types';
6
6
  import { DYNC_STATE_TABLE, type StateHelpers } from './StateManager';
@@ -11,7 +11,7 @@ export interface PushContext {
11
11
  state: StateHelpers;
12
12
  table: <T>(name: string) => StorageTable<T>;
13
13
  withTransaction: WithTransaction;
14
- syncOptions: SyncOptions;
14
+ syncOptions: ResolvedSyncOptions;
15
15
  }
16
16
 
17
17
  export interface PushAllContext extends PushContext {
@@ -125,7 +125,7 @@ async function pushOne(change: PendingChange, ctx: PushAllContext): Promise<void
125
125
 
126
126
  async function handleMissingRemoteRecord(change: PendingChange, ctx: PushContext): Promise<void> {
127
127
  const { tableName, localId } = change;
128
- const strategy = ctx.syncOptions.missingRemoteRecordDuringUpdateStrategy!;
128
+ const strategy = ctx.syncOptions.missingRemoteRecordDuringUpdateStrategy;
129
129
 
130
130
  let localItem: any;
131
131
 
@@ -261,8 +261,15 @@ async function processBatchPushResult(change: PendingChange, result: BatchPushRe
261
261
 
262
262
  if (!result.success) {
263
263
  if (action === SyncAction.Update) {
264
- // Update failed - might be missing remote record
265
- await handleMissingRemoteRecord(change, ctx);
264
+ if (!result.error) {
265
+ // Explicit "not found" from the server (api.update returned false, no error message).
266
+ // Matches per-table behaviour where api.update() returning false triggers this path.
267
+ await handleMissingRemoteRecord(change, ctx);
268
+ } else {
269
+ // Transient error on the server — leave the pending change in place so the next sync cycle retries,
270
+ // matching per-table behaviour.
271
+ ctx.logger.warn(`[dync] push:batch:update-failed tableName=${tableName} localId=${localId} error=${result.error}`);
272
+ }
266
273
  } else {
267
274
  ctx.logger.warn(`[dync] push:batch:failed tableName=${tableName} localId=${localId} error=${result.error}`);
268
275
  }
@@ -271,11 +278,11 @@ async function processBatchPushResult(change: PendingChange, result: BatchPushRe
271
278
 
272
279
  switch (action) {
273
280
  case SyncAction.Remove:
274
- handleRemoveSuccess(change, ctx);
281
+ await handleRemoveSuccess(change, ctx);
275
282
  break;
276
283
 
277
284
  case SyncAction.Update:
278
- handleUpdateSuccess(change, ctx);
285
+ await handleUpdateSuccess(change, ctx);
279
286
  break;
280
287
 
281
288
  case SyncAction.Create: {
@@ -3,6 +3,8 @@ import { SyncAction, type MutationEvent, type SyncedRecord } from '../types';
3
3
  import type { AddItem, StorageTable } from '../storage/types';
4
4
  import { DYNC_STATE_TABLE, type StateHelpers } from './StateManager';
5
5
  import type { WithTransaction } from './types';
6
+ import { SyncAwareCollection } from './SyncAwareCollection';
7
+ import { SyncAwareWhereClause } from './SyncAwareWhereClause';
6
8
  export type EmitMutation = (event: MutationEvent) => void;
7
9
 
8
10
  /**
@@ -463,5 +465,21 @@ export function enhanceSyncTable<T>({ table, tableName, withTransaction, state,
463
465
  table.bulkDelete = wrappedBulkDelete;
464
466
  table.clear = wrappedClear;
465
467
 
468
+ // Wrap collection-returning methods so that modify() and delete() on
469
+ // any derived collection are also sync-aware.
470
+ const originalWhere = table.where.bind(table);
471
+ const originalOrderBy = table.orderBy.bind(table);
472
+ const originalReverse = table.reverse.bind(table);
473
+ const originalOffset = table.offset.bind(table);
474
+ const originalLimit = table.limit.bind(table);
475
+ const originalJsFilter = table.jsFilter.bind(table);
476
+
477
+ table.where = (index: string) => new SyncAwareWhereClause(originalWhere(index), table);
478
+ table.orderBy = (index: string | string[]) => new SyncAwareCollection(originalOrderBy(index), table);
479
+ table.reverse = () => new SyncAwareCollection(originalReverse(), table);
480
+ table.offset = (n: number) => new SyncAwareCollection(originalOffset(n), table);
481
+ table.limit = (n: number) => new SyncAwareCollection(originalLimit(n), table);
482
+ table.jsFilter = (predicate: (item: T & SyncedRecord) => boolean) => new SyncAwareCollection(originalJsFilter(predicate), table);
483
+
466
484
  enhancedTables.add(tableName);
467
485
  }
@@ -4,7 +4,7 @@ import {
4
4
  type CrudSyncApi,
5
5
  type BatchSync,
6
6
  type DyncOptions,
7
- type SyncOptions,
7
+ type ResolvedSyncOptions,
8
8
  type SyncState,
9
9
  type SyncedRecord,
10
10
  type MissingRemoteRecordStrategy,
@@ -54,7 +54,7 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
54
54
  // Batch sync mode
55
55
  private batchSync?: BatchSync;
56
56
  private syncedTables: Set<string> = new Set();
57
- private syncOptions: SyncOptions;
57
+ private syncOptions: ResolvedSyncOptions;
58
58
  private logger: Logger;
59
59
  private syncTimerStarted = false;
60
60
  private mutationsDuringSync = false;
@@ -109,7 +109,7 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
109
109
  ...(options ?? {}),
110
110
  };
111
111
 
112
- this.logger = newLogger(this.syncOptions.logger!, this.syncOptions.minLogLevel!);
112
+ this.logger = newLogger(this.syncOptions.logger, this.syncOptions.minLogLevel);
113
113
  this.state = new StateManager({
114
114
  storageAdapter: this.adapter,
115
115
  });
@@ -395,7 +395,7 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
395
395
  const pullResult = await this.pullAll();
396
396
  const firstPushSyncError = await this.pushAll();
397
397
 
398
- // Emit pull mutation only for tables that had changes
398
+ // Emit pull mutation only for tables that had changes to trigger live query updates
399
399
  for (const tableName of pullResult.changedTables) {
400
400
  this.emitMutation({ type: 'pull', tableName });
401
401
  }
@@ -417,7 +417,7 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
417
417
  state: this.state,
418
418
  table: this.table.bind(this),
419
419
  withTransaction: this.withTransaction.bind(this),
420
- conflictResolutionStrategy: this.syncOptions.conflictResolutionStrategy!,
420
+ conflictResolutionStrategy: this.syncOptions.conflictResolutionStrategy,
421
421
  };
422
422
 
423
423
  if (this.batchSync) {
@@ -430,7 +430,7 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
430
430
  return runPullAll({
431
431
  ...baseContext,
432
432
  syncApis: this.syncApis,
433
- syncIntervalMs: this.syncOptions.syncIntervalMs!,
433
+ syncIntervalMs: this.syncOptions.syncIntervalMs,
434
434
  });
435
435
  }
436
436
 
@@ -471,7 +471,7 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
471
471
  while (this.syncTimerStarted) {
472
472
  this.sleepAbortController = new AbortController();
473
473
  await this.syncOnce();
474
- await sleep(this.syncOptions.syncIntervalMs!, this.sleepAbortController.signal);
474
+ await sleep(this.syncOptions.syncIntervalMs, this.sleepAbortController.signal);
475
475
  }
476
476
 
477
477
  this.syncStatus = 'disabled';
@@ -17,8 +17,8 @@ export class DexieAdapter implements StorageAdapter {
17
17
  }
18
18
 
19
19
  async open(): Promise<void> {
20
- // Dexie auto-opens on first operation, so this is typically a no-op.
21
- // However, after delete() we explicitly re-open to ensure continued usability.
20
+ // Dexie will auto-open on first operation
21
+ await requestPersistentStorage();
22
22
  }
23
23
 
24
24
  async close(): Promise<void> {
@@ -70,3 +70,14 @@ export class DexieAdapter implements StorageAdapter {
70
70
  });
71
71
  }
72
72
  }
73
+
74
+ async function requestPersistentStorage(): Promise<void> {
75
+ if (navigator.storage && navigator.storage.persist) {
76
+ const granted = await navigator.storage.persist();
77
+ if (granted) {
78
+ console.log('[dync] IndexedDB storage persistence granted');
79
+ } else {
80
+ console.warn('[dync] IndexedDB storage may be cleared under storage pressure');
81
+ }
82
+ }
83
+ }
@@ -290,12 +290,8 @@ export class SQLiteAdapter implements StorageAdapter {
290
290
  if (!debug) {
291
291
  return;
292
292
  }
293
- const hasParams = parameters && parameters.length;
294
- if (typeof debug === 'function') {
295
- debug(statement, hasParams ? parameters : undefined);
296
- return;
297
- }
298
293
  if (debug === true) {
294
+ const hasParams = parameters && parameters.length;
299
295
  if (hasParams) {
300
296
  console.debug('[dync][sqlite]', statement, parameters);
301
297
  } else {
@@ -1,7 +1,7 @@
1
1
  import type { SQLiteColumnDefinition, SQLiteTableDefinition } from './schema';
2
2
 
3
3
  export interface SQLiteAdapterOptions {
4
- debug?: boolean | ((statement: string, parameters?: any[]) => void);
4
+ debug?: boolean;
5
5
  }
6
6
 
7
7
  export interface SQLiteColumnSchema extends SQLiteColumnDefinition {
package/src/types.ts CHANGED
@@ -28,8 +28,8 @@ export interface CrudSyncApi {
28
28
  add: (item: any) => Promise<any | undefined>;
29
29
  update: (id: any, changes: any, item: any) => Promise<boolean>;
30
30
  remove: (id: any) => Promise<void>;
31
- list: (lastUpdatedAt: Date) => Promise<any[]>;
32
- // Optional: Extend `SyncOptions.syncIntervalMs` for this table's pull sync
31
+ list: (newestUpdatedAt: Date) => Promise<any[]>;
32
+ // Optional: Add `listExtraIntervalMs` to `SyncOptions.syncIntervalMs` for when this table is pulled during sync
33
33
  listExtraIntervalMs?: number;
34
34
  firstLoad?: (lastId: any) => Promise<any[]>;
35
35
  }
@@ -62,6 +62,7 @@ export interface BatchPushResult {
62
62
  id?: any;
63
63
  // Server-assigned updated_at (for successful adds/updates)
64
64
  updated_at?: string;
65
+ // Server-assigned transient error - client will retry push
65
66
  error?: string;
66
67
  }
67
68
 
@@ -124,6 +125,9 @@ export interface SyncOptions {
124
125
  conflictResolutionStrategy?: ConflictResolutionStrategy;
125
126
  }
126
127
 
128
+ export type ResolvedSyncOptions = SyncOptions &
129
+ Required<Pick<SyncOptions, 'syncIntervalMs' | 'logger' | 'minLogLevel' | 'missingRemoteRecordDuringUpdateStrategy' | 'conflictResolutionStrategy'>>;
130
+
127
131
  export interface DyncOptions<TStoreMap extends Record<string, any> = Record<string, any>> {
128
132
  databaseName: string;
129
133
  storageAdapter: StorageAdapter;
@@ -151,7 +155,9 @@ export type SyncApi = {
151
155
  };
152
156
 
153
157
  export interface MutationEvent {
154
- type: 'add' | 'update' | 'delete' | 'bulkAdd' | 'bulkPut' | 'bulkDelete' | 'clear' | 'put' | 'modify' | 'pull';
158
+ // 'pull' as a mutation type indicates that changes have been pulled from the server,
159
+ // therefore trigger live query updates for them
160
+ type: 'add' | 'update' | 'delete' | 'pull';
155
161
  tableName: string;
156
162
  keys?: unknown[];
157
163
  }