@anfenn/dync 1.0.27 → 1.0.30

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.
@@ -1,4 +1,4 @@
1
- import { a as SyncApi, k as SyncState } from '../types-yJuzCcux.cjs';
1
+ import { a as SyncApi, k as SyncState } from '../types-DW42y281.cjs';
2
2
  import '../dexie-T9m1mP1h.cjs';
3
3
  import 'dexie';
4
4
  import '../types-CSbIAfu2.cjs';
@@ -1,4 +1,4 @@
1
- import { a as SyncApi, k as SyncState } from '../types-BLw25Wlc.js';
1
+ import { a as SyncApi, k as SyncState } from '../types-n8Zge2zF.js';
2
2
  import '../dexie-BFPA0JU2.js';
3
3
  import 'dexie';
4
4
  import '../types-CSbIAfu2.js';
@@ -94,36 +94,10 @@ interface SyncOptions {
94
94
  onAfterMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback;
95
95
  conflictResolutionStrategy?: ConflictResolutionStrategy;
96
96
  }
97
- /**
98
- * Configuration options for creating a Dync instance.
99
- *
100
- * @example Per-table sync mode
101
- * ```ts
102
- * const db = new Dync<Store>({
103
- * databaseName: 'my-app',
104
- * storageAdapter: new SQLiteAdapter(driver),
105
- * sync: { todos: todoSyncApi },
106
- * });
107
- * ```
108
- *
109
- * @example Batch sync mode
110
- * ```ts
111
- * const db = new Dync<Store>({
112
- * databaseName: 'my-app',
113
- * storageAdapter: new SQLiteAdapter(driver),
114
- * sync: { syncTables: ['todos'], push, pull },
115
- * });
116
- * ```
117
- */
118
97
  interface DyncOptions<TStoreMap extends Record<string, any> = Record<string, any>> {
119
98
  databaseName: string;
120
99
  storageAdapter: StorageAdapter;
121
- /**
122
- * Sync configuration - either per-table APIs or batch sync.
123
- * Per-table: `{ tableName: { add, update, remove, list } }`
124
- * Batch: `{ syncTables: [...], push, pull }`
125
- */
126
- sync: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
100
+ sync?: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
127
101
  options?: SyncOptions;
128
102
  }
129
103
  interface FirstLoadProgress {
@@ -136,7 +110,6 @@ type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
136
110
  type SyncApi = {
137
111
  enable: (enabled: boolean) => Promise<void>;
138
112
  startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
139
- /** Current sync state - use useSyncState() hook for reactive updates in React */
140
113
  readonly state: SyncState;
141
114
  resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
142
115
  onStateChange: (fn: (state: SyncState) => void) => () => void;
@@ -94,36 +94,10 @@ interface SyncOptions {
94
94
  onAfterMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback;
95
95
  conflictResolutionStrategy?: ConflictResolutionStrategy;
96
96
  }
97
- /**
98
- * Configuration options for creating a Dync instance.
99
- *
100
- * @example Per-table sync mode
101
- * ```ts
102
- * const db = new Dync<Store>({
103
- * databaseName: 'my-app',
104
- * storageAdapter: new SQLiteAdapter(driver),
105
- * sync: { todos: todoSyncApi },
106
- * });
107
- * ```
108
- *
109
- * @example Batch sync mode
110
- * ```ts
111
- * const db = new Dync<Store>({
112
- * databaseName: 'my-app',
113
- * storageAdapter: new SQLiteAdapter(driver),
114
- * sync: { syncTables: ['todos'], push, pull },
115
- * });
116
- * ```
117
- */
118
97
  interface DyncOptions<TStoreMap extends Record<string, any> = Record<string, any>> {
119
98
  databaseName: string;
120
99
  storageAdapter: StorageAdapter;
121
- /**
122
- * Sync configuration - either per-table APIs or batch sync.
123
- * Per-table: `{ tableName: { add, update, remove, list } }`
124
- * Batch: `{ syncTables: [...], push, pull }`
125
- */
126
- sync: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
100
+ sync?: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
127
101
  options?: SyncOptions;
128
102
  }
129
103
  interface FirstLoadProgress {
@@ -136,7 +110,6 @@ type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
136
110
  type SyncApi = {
137
111
  enable: (enabled: boolean) => Promise<void>;
138
112
  startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
139
- /** Current sync state - use useSyncState() hook for reactive updates in React */
140
113
  readonly state: SyncState;
141
114
  resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
142
115
  onStateChange: (fn: (state: SyncState) => void) => () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anfenn/dync",
3
- "version": "1.0.27",
3
+ "version": "1.0.30",
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": [
@@ -132,8 +132,8 @@
132
132
  "LICENSE"
133
133
  ],
134
134
  "scripts": {
135
- "build": "pnpm run format && pnpm run lint && pnpm exec tsc --noEmit && tsup",
136
- "build:all": "pnpm run build && pnpm --filter react-capacitor build",
135
+ "build": "pnpm format && pnpm lint && pnpm exec tsc --noEmit && tsup",
136
+ "build:all": "pnpm build && pnpm --filter react-capacitor build",
137
137
  "dev": "tsup --watch",
138
138
  "lint": "eslint . --fix \"src/**/*.{js,jsx,ts,tsx,json}\"",
139
139
  "format": "prettier --write \"**/*.{ts,tsx,md}\"",
@@ -142,7 +142,9 @@
142
142
  "test:all": "pnpm --filter tests test:all",
143
143
  "test:ci": "pnpm --filter tests test:ci",
144
144
  "test:browser:full": "pnpm --filter tests test:browser:full",
145
- "reinstall": "find . -name node_modules -type d -prune -exec rm -rf {} + && pnpm install"
145
+ "reinstall": "find . -name node_modules -type d -prune -exec rm -rf {} + && pnpm install",
146
+ "push": "bash -c '[ -n \"$1\" ] && git add . && git commit -m \"$1\" && git push' --",
147
+ "release": "bash -c '[ -n \"$1\" ] || { echo \"Usage: pnpm release <commit-message>\"; exit 1; } && pnpm build:all && pnpm test:all && pnpm test:browser:full && git add . && git commit -m \"$1\" && npm version patch && git push && git push --tags && npm publish' --"
146
148
  },
147
149
  "devDependencies": {
148
150
  "@capacitor-community/sqlite": "7.0.2",
@@ -6,7 +6,7 @@ import type { WithTransaction } from './types';
6
6
  export type EmitMutation = (event: MutationEvent) => void;
7
7
 
8
8
  /**
9
- * Wraps a table with mutation emission for reactive updates.
9
+ * Wraps a table with mutation emission for reactive updates and auto-generates _localId.
10
10
  * This allows useLiveQuery to work with any table.
11
11
  */
12
12
  export function wrapWithMutationEmitter<T>(table: StorageTable<T>, tableName: string, emitMutation: EmitMutation): void {
@@ -21,13 +21,15 @@ export function wrapWithMutationEmitter<T>(table: StorageTable<T>, tableName: st
21
21
  const rawClear = table.raw.clear;
22
22
 
23
23
  table.add = async (item: AddItem<T>) => {
24
- const result = await rawAdd(item as T);
24
+ const itemWithLocalId = { ...item, _localId: (item as any)._localId || createLocalId() } as T;
25
+ const result = await rawAdd(itemWithLocalId);
25
26
  emitMutation({ type: 'add', tableName, keys: [result] });
26
27
  return result;
27
28
  };
28
29
 
29
30
  table.put = async (item: T) => {
30
- const result = await rawPut(item);
31
+ const itemWithLocalId = { ...item, _localId: (item as any)._localId || createLocalId() } as T;
32
+ const result = await rawPut(itemWithLocalId);
31
33
  emitMutation({ type: 'update', tableName, keys: [result] });
32
34
  return result;
33
35
  };
@@ -46,7 +48,11 @@ export function wrapWithMutationEmitter<T>(table: StorageTable<T>, tableName: st
46
48
  };
47
49
 
48
50
  table.bulkAdd = async (items: AddItem<T>[]) => {
49
- const result = await rawBulkAdd(items as T[]);
51
+ const itemsWithLocalIds = items.map((item) => ({
52
+ ...item,
53
+ _localId: (item as any)._localId || createLocalId(),
54
+ })) as T[];
55
+ const result = await rawBulkAdd(itemsWithLocalIds);
50
56
  if (items.length > 0) {
51
57
  emitMutation({ type: 'add', tableName });
52
58
  }
@@ -54,7 +60,11 @@ export function wrapWithMutationEmitter<T>(table: StorageTable<T>, tableName: st
54
60
  };
55
61
 
56
62
  table.bulkPut = async (items: T[]) => {
57
- const result = await rawBulkPut(items);
63
+ const itemsWithLocalIds = items.map((item) => ({
64
+ ...item,
65
+ _localId: (item as any)._localId || createLocalId(),
66
+ })) as T[];
67
+ const result = await rawBulkPut(itemsWithLocalIds);
58
68
  if (items.length > 0) {
59
69
  emitMutation({ type: 'update', tableName });
60
70
  }
@@ -85,15 +85,17 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
85
85
  constructor(config: DyncOptions<_TStoreMap>) {
86
86
  const { databaseName, storageAdapter, sync: syncConfig, options } = config;
87
87
 
88
- // Detect mode based on whether sync config has batch sync shape
89
- const isBatchMode = typeof (syncConfig as BatchSync).push === 'function' && typeof (syncConfig as BatchSync).pull === 'function';
90
-
91
- if (isBatchMode) {
92
- this.batchSync = syncConfig as BatchSync;
93
- this.syncedTables = new Set(this.batchSync.syncTables);
94
- } else {
95
- this.syncApis = syncConfig as Record<string, ApiFunctions>;
96
- this.syncedTables = new Set(Object.keys(this.syncApis));
88
+ if (syncConfig) {
89
+ // Detect mode based on whether sync config has batch sync shape
90
+ const isBatchMode = typeof syncConfig.push === 'function' && typeof syncConfig.pull === 'function';
91
+
92
+ if (isBatchMode) {
93
+ this.batchSync = syncConfig as BatchSync;
94
+ this.syncedTables = new Set(this.batchSync.syncTables);
95
+ } else {
96
+ this.syncApis = syncConfig as Record<string, ApiFunctions>;
97
+ this.syncedTables = new Set(Object.keys(this.syncApis));
98
+ }
97
99
  }
98
100
 
99
101
  this.adapter = storageAdapter;
@@ -158,6 +160,9 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
158
160
  // Auto-inject sync fields for sync tables
159
161
  // Note: updated_at is indexed to support user queries like orderBy('updated_at')
160
162
  fullSchema[tableName] = `${LOCAL_PK}, &${SERVER_PK}, ${tableSchema}, ${UPDATED_AT}`;
163
+ } else {
164
+ // Auto-inject _localId as primary key for non-sync tables
165
+ fullSchema[tableName] = `${LOCAL_PK}, ${tableSchema}`;
161
166
  }
162
167
 
163
168
  self.logger.debug(
@@ -167,6 +172,9 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
167
172
  if (isSyncTable) {
168
173
  // Auto-inject sync columns for structured schemas
169
174
  fullSchema[tableName] = self.injectSyncColumns(tableSchema);
175
+ } else {
176
+ // Auto-inject _localId column for non-sync structured schemas
177
+ fullSchema[tableName] = self.injectLocalIdColumn(tableSchema);
170
178
  }
171
179
 
172
180
  const schemaColumns = Object.keys((fullSchema[tableName] as SQLiteTableDefinition).columns ?? {}).join(', ');
@@ -350,6 +358,26 @@ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
350
358
  };
351
359
  }
352
360
 
361
+ private injectLocalIdColumn(schema: SQLiteTableDefinition): SQLiteTableDefinition {
362
+ const columns = schema.columns ?? {};
363
+
364
+ // Validate user hasn't defined reserved _localId column
365
+ if (columns[LOCAL_PK]) {
366
+ throw new Error(`Column '${LOCAL_PK}' is auto-injected and cannot be defined manually.`);
367
+ }
368
+
369
+ // Inject _localId column as primary key
370
+ const injectedColumns: Record<string, any> = {
371
+ ...columns,
372
+ [LOCAL_PK]: { type: 'TEXT' },
373
+ };
374
+
375
+ return {
376
+ ...schema,
377
+ columns: injectedColumns,
378
+ };
379
+ }
380
+
353
381
  private enhanceSyncTable<T>(table: StorageTable<T & SyncedRecord>, tableName: string): void {
354
382
  enhanceSyncTable({
355
383
  table,
package/src/types.ts CHANGED
@@ -122,36 +122,10 @@ export interface SyncOptions {
122
122
  conflictResolutionStrategy?: ConflictResolutionStrategy;
123
123
  }
124
124
 
125
- /**
126
- * Configuration options for creating a Dync instance.
127
- *
128
- * @example Per-table sync mode
129
- * ```ts
130
- * const db = new Dync<Store>({
131
- * databaseName: 'my-app',
132
- * storageAdapter: new SQLiteAdapter(driver),
133
- * sync: { todos: todoSyncApi },
134
- * });
135
- * ```
136
- *
137
- * @example Batch sync mode
138
- * ```ts
139
- * const db = new Dync<Store>({
140
- * databaseName: 'my-app',
141
- * storageAdapter: new SQLiteAdapter(driver),
142
- * sync: { syncTables: ['todos'], push, pull },
143
- * });
144
- * ```
145
- */
146
125
  export interface DyncOptions<TStoreMap extends Record<string, any> = Record<string, any>> {
147
126
  databaseName: string;
148
127
  storageAdapter: StorageAdapter;
149
- /**
150
- * Sync configuration - either per-table APIs or batch sync.
151
- * Per-table: `{ tableName: { add, update, remove, list } }`
152
- * Batch: `{ syncTables: [...], push, pull }`
153
- */
154
- sync: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
128
+ sync?: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
155
129
  options?: SyncOptions;
156
130
  }
157
131
 
@@ -167,7 +141,7 @@ export type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
167
141
  export type SyncApi = {
168
142
  enable: (enabled: boolean) => Promise<void>;
169
143
  startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
170
- /** Current sync state - use useSyncState() hook for reactive updates in React */
144
+ // Current sync state - use useSyncState() hook for reactive updates in React
171
145
  readonly state: SyncState;
172
146
  resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
173
147
  onStateChange: (fn: (state: SyncState) => void) => () => void;