@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.
- package/README.md +4 -3
- package/dist/capacitor.d.cts +1 -1
- package/dist/capacitor.d.ts +1 -1
- package/dist/{dexie-B3Ihrrxi.d.ts → dexie-Cxn4kUoF.d.ts} +1 -1
- package/dist/{dexie-BRWUYM02.d.cts → dexie-D8u9cGSy.d.cts} +1 -1
- package/dist/dexie.cjs +11 -0
- package/dist/dexie.cjs.map +1 -1
- package/dist/dexie.d.cts +2 -2
- package/dist/dexie.d.ts +2 -2
- package/dist/dexie.js +11 -0
- package/dist/dexie.js.map +1 -1
- package/dist/expoSqlite.d.cts +1 -1
- package/dist/expoSqlite.d.ts +1 -1
- package/dist/index.cjs +188 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +188 -8
- package/dist/index.js.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/react/index.d.cts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/{types-DhlgTu1o.d.ts → types-Da3ZhO9Y.d.cts} +3 -3
- package/dist/{types-6-NyRQ0D.d.cts → types-DcEg2pvl.d.cts} +1 -1
- package/dist/{types-6-NyRQ0D.d.ts → types-DcEg2pvl.d.ts} +1 -1
- package/dist/{types-Di82FTAL.d.cts → types-DnHiaBZV.d.ts} +3 -3
- package/dist/wa-sqlite.d.cts +1 -1
- package/dist/wa-sqlite.d.ts +1 -1
- package/package.json +2 -2
- package/src/core/SyncAwareCollection.ts +124 -0
- package/src/core/SyncAwareWhereClause.ts +66 -0
- package/src/core/pushOperations.ts +14 -7
- package/src/core/tableEnhancers.ts +18 -0
- package/src/index.shared.ts +7 -7
- package/src/storage/dexie/DexieAdapter.ts +13 -2
- package/src/storage/sqlite/SQLiteAdapter.ts +1 -5
- package/src/storage/sqlite/types.ts +1 -1
- 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-
|
|
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-
|
|
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.
|
package/dist/react/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as SyncApi, k as SyncState } from '../types-
|
|
2
|
-
import '../dexie-
|
|
1
|
+
import { a as SyncApi, k as SyncState } from '../types-Da3ZhO9Y.cjs';
|
|
2
|
+
import '../dexie-D8u9cGSy.cjs';
|
|
3
3
|
import 'dexie';
|
|
4
|
-
import '../types-
|
|
4
|
+
import '../types-DcEg2pvl.cjs';
|
|
5
5
|
|
|
6
6
|
/** Minimal database interface for React hooks */
|
|
7
7
|
interface DyncLike {
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as SyncApi, k as SyncState } from '../types-
|
|
2
|
-
import '../dexie-
|
|
1
|
+
import { a as SyncApi, k as SyncState } from '../types-DnHiaBZV.js';
|
|
2
|
+
import '../dexie-Cxn4kUoF.js';
|
|
3
3
|
import 'dexie';
|
|
4
|
-
import '../types-
|
|
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-
|
|
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: (
|
|
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' | '
|
|
120
|
+
type: 'add' | 'update' | 'delete' | 'pull';
|
|
121
121
|
tableName: string;
|
|
122
122
|
keys?: unknown[];
|
|
123
123
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as StorageAdapter, a as StorageTable } from './dexie-
|
|
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: (
|
|
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' | '
|
|
120
|
+
type: 'add' | 'update' | 'delete' | 'pull';
|
|
121
121
|
tableName: string;
|
|
122
122
|
keys?: unknown[];
|
|
123
123
|
}
|
package/dist/wa-sqlite.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-
|
|
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.
|
package/dist/wa-sqlite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SQLiteDatabaseDriver, a as SQLiteRunResult, b as SQLiteQueryResult } from './types-
|
|
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
|
+
"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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
265
|
-
|
|
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
|
}
|
package/src/index.shared.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type CrudSyncApi,
|
|
5
5
|
type BatchSync,
|
|
6
6
|
type DyncOptions,
|
|
7
|
-
type
|
|
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:
|
|
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
|
|
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
|
|
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-
|
|
21
|
-
|
|
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
|
|
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: (
|
|
32
|
-
// Optional:
|
|
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
|
-
|
|
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
|
}
|