@absolutejs/sync-capacitor 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/android/src/main/java/js/absolute/sync/AbsoluteBackgroundSyncEngine.java +20 -9
- package/dist/index.d.ts +10 -2
- package/dist/index.js +663 -19
- package/dist/index.js.map +7 -4
- package/ios/Sources/AbsoluteSyncCapacitor/AbsoluteBackgroundSyncEngine.swift +21 -9
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -14,13 +14,16 @@ import { createSyncClient } from '@absolutejs/sync/client';
|
|
|
14
14
|
import { lifecycle, network } from '@absolutejs/devices';
|
|
15
15
|
import {
|
|
16
16
|
createCapacitorSyncLocalStore,
|
|
17
|
+
createCapacitorSyncProtection,
|
|
17
18
|
installCapacitorSyncLifecycle
|
|
18
19
|
} from '@absolutejs/sync-capacitor';
|
|
19
20
|
|
|
20
21
|
const client = createSyncClient({
|
|
21
22
|
url: 'wss://app.example.com/sync/ws',
|
|
22
23
|
durable: {
|
|
23
|
-
store: createCapacitorSyncLocalStore(
|
|
24
|
+
store: createCapacitorSyncLocalStore({
|
|
25
|
+
protection: createCapacitorSyncProtection()
|
|
26
|
+
}),
|
|
24
27
|
namespace: authenticatedPrincipalNamespace
|
|
25
28
|
}
|
|
26
29
|
});
|
|
@@ -38,6 +41,14 @@ failures obey the client's delivery ceiling; conflicts and permanent failures
|
|
|
38
41
|
remain in the principal's dead-letter partition for explicit remediation rather
|
|
39
42
|
than replaying forever.
|
|
40
43
|
|
|
44
|
+
Generated mutation conflict policies are captured inside each encrypted outbox
|
|
45
|
+
record. The TypeScript foreground/headless runner and the Android WorkManager or
|
|
46
|
+
iOS BGProcessingTask runner therefore make the same bounded decision: manual
|
|
47
|
+
conflicts become dead letters, `server-wins` discards the rejected local intent,
|
|
48
|
+
and `client-wins` retries the unchanged operation ID up to its declared ceiling.
|
|
49
|
+
An explicit argument-changing rebase is left to the foreground remediation API
|
|
50
|
+
and creates a new traceable intent.
|
|
51
|
+
|
|
41
52
|
The namespace must come from a verified Auth principal, never from an untrusted
|
|
42
53
|
route or form value. AbsoluteJS derives an opaque namespace from the verified
|
|
43
54
|
issuer, public client ID, and subject. Signing out locks that partition by
|
|
@@ -86,6 +97,22 @@ deterministic component bundle. SQLite tracks each component independently,
|
|
|
86
97
|
keeps removed-pack ledgers as orphan diagnostics, and migrates all records and
|
|
87
98
|
ledger updates in one transaction.
|
|
88
99
|
|
|
100
|
+
## Protected local data and quotas
|
|
101
|
+
|
|
102
|
+
AbsoluteJS automatically installs `createCapacitorSyncProtection()`. It creates
|
|
103
|
+
one random AES-256-GCM data key, seals that key in the existing iOS Keychain or
|
|
104
|
+
Android Keystore vault, and writes only authenticated ciphertext envelopes to
|
|
105
|
+
SQLite. Namespace, record kind, and collection/mutation name are authenticated
|
|
106
|
+
as associated data. The foreground adapter and both finite native workers use
|
|
107
|
+
the same versioned format; a missing key, changed identity, or modified record
|
|
108
|
+
fails closed. Direct Capacitor integrations opt in as shown above.
|
|
109
|
+
|
|
110
|
+
The generated `localData` policy also enforces memory-only records,
|
|
111
|
+
whole-projection expiry, deterministic eviction priority, and a logical
|
|
112
|
+
per-principal byte ceiling. Pending mutations are never evicted. When protected
|
|
113
|
+
data declares a memory-only fallback, browsers without an audited key provider
|
|
114
|
+
remain usable without writing that data to disk.
|
|
115
|
+
|
|
89
116
|
## Managed native background Sync
|
|
90
117
|
|
|
91
118
|
AbsoluteJS also configures `AbsoluteBackgroundSync` when the application uses
|
|
@@ -130,9 +157,10 @@ generated regions. Android scheduling is registered by the plugin.
|
|
|
130
157
|
|
|
131
158
|
## Platform notes
|
|
132
159
|
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
- AbsoluteJS protects record payloads with AES-256-GCM rather than claiming the
|
|
161
|
+
default SQLite connection is SQLCipher-encrypted. SQLite keys, namespaces,
|
|
162
|
+
and ordering columns remain metadata; sensitive row and mutation payloads are
|
|
163
|
+
ciphertext. Review platform encryption-export requirements before shipping.
|
|
136
164
|
- Web/PWA builds should use `createIndexedDbSyncLocalStore` from
|
|
137
165
|
`@absolutejs/sync/client`; they do not need the plugin's WASM/web component.
|
|
138
166
|
- The adapter serializes transactions so an app cannot overlap two explicit
|
|
@@ -16,6 +16,12 @@ import java.nio.charset.StandardCharsets;
|
|
|
16
16
|
import java.util.HashMap;
|
|
17
17
|
import java.util.LinkedHashMap;
|
|
18
18
|
import java.util.Map;
|
|
19
|
+
import java.util.Arrays;
|
|
20
|
+
import java.security.SecureRandom;
|
|
21
|
+
import javax.crypto.Cipher;
|
|
22
|
+
import javax.crypto.spec.GCMParameterSpec;
|
|
23
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
24
|
+
import android.util.Base64;
|
|
19
25
|
import js.absolute.devices.AbsoluteSecureStorageVault;
|
|
20
26
|
import org.json.JSONArray;
|
|
21
27
|
import org.json.JSONObject;
|
|
@@ -23,6 +29,7 @@ import org.json.JSONObject;
|
|
|
23
29
|
final class AbsoluteBackgroundSyncEngine {
|
|
24
30
|
static final class NoSession extends Exception {}
|
|
25
31
|
private static final int MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
32
|
+
private static final String DATA_KEY="absolutejs.sync.data-key.v1";
|
|
26
33
|
private static final String RUNNING="running", LAST_RUN="lastRunAt", LAST_ERROR="lastError", LAST_ACK="lastAcknowledged", LAST_PULL="lastPulled";
|
|
27
34
|
private static final class Mutation { final String id; final int attempts; Mutation(String i,int a){id=i;attempts=a;} }
|
|
28
35
|
private static final class Batch { final JSONObject request=new JSONObject(); final Map<String,Mutation> mutations=new LinkedHashMap<>(); final Map<String,JSONObject> pulls=new LinkedHashMap<>(); }
|
|
@@ -38,7 +45,7 @@ final class AbsoluteBackgroundSyncEngine {
|
|
|
38
45
|
AbsoluteBackgroundSyncConfig.preferences(context).edit().putBoolean(RUNNING,true).remove(LAST_ERROR).apply();
|
|
39
46
|
try{
|
|
40
47
|
String access=refresh(context,config); SQLiteDatabase db=SQLiteDatabase.openDatabase(context.getDatabasePath(config.databaseName+"SQLite.db").getPath(),null,SQLiteDatabase.OPEN_READWRITE);
|
|
41
|
-
try{db.execSQL("PRAGMA busy_timeout=5000"); Batch batch=prepare(db,config); JSONObject response=request(config.endpoint,"POST",batch.request.toString().getBytes(StandardCharsets.UTF_8),"application/json","Bearer "+access,config.issuer); int[] counts=settle(db,config,batch,response);
|
|
48
|
+
try{db.execSQL("PRAGMA busy_timeout=5000"); Batch batch=prepare(context,db,config); JSONObject response=request(config.endpoint,"POST",batch.request.toString().getBytes(StandardCharsets.UTF_8),"application/json","Bearer "+access,config.issuer); int[] counts=settle(context,db,config,batch,response);
|
|
42
49
|
AbsoluteBackgroundSyncConfig.preferences(context).edit().putBoolean(RUNNING,false).putLong(LAST_RUN,System.currentTimeMillis()).putInt(LAST_ACK,counts[0]).putInt(LAST_PULL,counts[1]).remove(LAST_ERROR).apply();
|
|
43
50
|
}finally{db.close();}
|
|
44
51
|
}catch(NoSession e){AbsoluteBackgroundSyncConfig.preferences(context).edit().putBoolean(RUNNING,false).remove(LAST_ERROR).apply();throw e;}
|
|
@@ -56,24 +63,24 @@ final class AbsoluteBackgroundSyncEngine {
|
|
|
56
63
|
}
|
|
57
64
|
private static String form(String n,String v)throws Exception{return URLEncoder.encode(n,"UTF-8")+"="+URLEncoder.encode(v,"UTF-8");}
|
|
58
65
|
|
|
59
|
-
private static Batch prepare(SQLiteDatabase db,AbsoluteBackgroundSyncConfig config)throws Exception{
|
|
66
|
+
private static Batch prepare(Context context,SQLiteDatabase db,AbsoluteBackgroundSyncConfig config)throws Exception{
|
|
60
67
|
Batch b=new Batch(); JSONArray mutations=new JSONArray(),pulls=new JSONArray(); long now=System.currentTimeMillis(); db.beginTransaction();
|
|
61
68
|
try{
|
|
62
69
|
try(Cursor c=db.rawQuery("SELECT operation_id,record_json FROM absolute_sync_mutations WHERE namespace=? ORDER BY created_at,operation_id",new String[]{config.namespace})){
|
|
63
|
-
while(c.moveToNext()&&mutations.length()<config.maxMutations){String id=c.getString(0);JSONObject r=
|
|
70
|
+
while(c.moveToNext()&&mutations.length()<config.maxMutations){String id=c.getString(0);JSONObject r=decodeRecord(context,c.getString(1),"mutation",config.namespace);if("dead-letter".equals(r.optString("state"))||r.optLong("nextAttemptAt",0)>now)continue;int attempts=r.optInt("attempts",0)+1;r.put("attempts",attempts).remove("nextAttemptAt");updateMutation(context,db,config.namespace,id,r);JSONObject m=new JSONObject().put("operationId",id).put("name",r.getString("name"));if(r.has("args"))m.put("args",r.get("args"));mutations.put(m);b.mutations.put(id,new Mutation(id,attempts));}
|
|
64
71
|
}
|
|
65
72
|
try(Cursor c=db.rawQuery("SELECT collection_key,record_json FROM absolute_sync_collections WHERE namespace=? ORDER BY collection_key",new String[]{config.namespace})){
|
|
66
|
-
while(c.moveToNext()&&pulls.length()<config.maxPulls){String key=c.getString(0);JSONObject r=
|
|
73
|
+
while(c.moveToNext()&&pulls.length()<config.maxPulls){String key=c.getString(0);JSONObject r=decodeRecord(context,c.getString(1),"collection",config.namespace);String collection=r.optString("collection");if(collection.isEmpty())continue;JSONObject p=new JSONObject().put("id",key).put("collection",collection);if(r.has("params"))p.put("params",r.get("params"));if("id".equals(r.optString("headlessKey"))){if(r.has("cursor"))p.put("since",r.get("cursor"));else if(r.optInt("version",0)>0)p.put("since",r.getInt("version"));}pulls.put(p);b.pulls.put(key,r);}
|
|
67
74
|
}
|
|
68
75
|
db.setTransactionSuccessful();
|
|
69
76
|
}finally{db.endTransaction();}
|
|
70
77
|
b.request.put("version",1).put("mutations",mutations).put("pulls",pulls);return b;
|
|
71
78
|
}
|
|
72
|
-
private static int[] settle(SQLiteDatabase db,AbsoluteBackgroundSyncConfig config,Batch b,JSONObject response)throws Exception{
|
|
79
|
+
private static int[] settle(Context context,SQLiteDatabase db,AbsoluteBackgroundSyncConfig config,Batch b,JSONObject response)throws Exception{
|
|
73
80
|
if(response.getInt("version")!=1)throw new IllegalStateException("Unsupported Headless Sync response.");Map<String,JSONObject> mutationResults=index(response.getJSONArray("mutations"),"operationId"),pullResults=index(response.getJSONArray("pulls"),"id");int ack=0,pulled=0;db.beginTransaction();
|
|
74
81
|
try{
|
|
75
|
-
for(Mutation sent:b.mutations.values()){JSONObject current=record(db,"absolute_sync_mutations","operation_id",config.namespace,sent.id);if(current==null||current.optInt("attempts",-1)!=sent.attempts)continue;JSONObject outcome=mutationResults.get(sent.id);if(outcome!=null&&"ack".equals(outcome.optString("status"))){db.delete("absolute_sync_mutations","namespace=? AND operation_id=?",new String[]{config.namespace,sent.id});ack++;continue;}JSONObject rejection=outcome==null?new JSONObject().put("kind","retryable").put("message","Missing mutation response."):outcome.getJSONObject("rejection");current.put("lastError",rejection.optString("message","Mutation rejected.")).put("rejection",rejection);if(!"retryable".equals(rejection.optString("kind"))||sent.attempts>=config.maxAttempts)current.put("state","dead-letter").put("deadLetteredAt",System.currentTimeMillis()).remove("nextAttemptAt");else current.put("state","pending").put("nextAttemptAt",System.currentTimeMillis()+Math.max(0,rejection.optLong("retryAfterMs",Math.min(30000,500L<<Math.min(20,sent.attempts-1)))));updateMutation(db,config.namespace,sent.id,current);}
|
|
76
|
-
for(Map.Entry<String,JSONObject> requested:b.pulls.entrySet()){JSONObject result=pullResults.get(requested.getKey());if(result==null||"error".equals(result.optString("type")))continue;JSONObject current=record(db,"absolute_sync_collections","collection_key",config.namespace,requested.getKey());if(!same(current,requested.getValue()))continue;ContentValues values=new ContentValues();values.put("record_json",
|
|
82
|
+
for(Mutation sent:b.mutations.values()){JSONObject current=record(context,db,"absolute_sync_mutations","operation_id","mutation",config.namespace,sent.id);if(current==null||current.optInt("attempts",-1)!=sent.attempts)continue;JSONObject outcome=mutationResults.get(sent.id);if(outcome!=null&&"ack".equals(outcome.optString("status"))){db.delete("absolute_sync_mutations","namespace=? AND operation_id=?",new String[]{config.namespace,sent.id});ack++;continue;}JSONObject rejection=outcome==null?new JSONObject().put("kind","retryable").put("message","Missing mutation response."):outcome.getJSONObject("rejection");current.put("lastError",rejection.optString("message","Mutation rejected.")).put("rejection",rejection);JSONObject conflictPolicy=current.optJSONObject("conflictPolicy");String conflictStrategy=conflictPolicy==null?"manual":conflictPolicy.optString("strategy","manual");if("conflict".equals(rejection.optString("kind"))&&"server-wins".equals(conflictStrategy)){db.delete("absolute_sync_mutations","namespace=? AND operation_id=?",new String[]{config.namespace,sent.id});continue;}if("conflict".equals(rejection.optString("kind"))&&"client-wins".equals(conflictStrategy)&¤t.optInt("conflictAttempts",0)<conflictPolicy.optInt("maxAttempts",1)){current.put("conflictAttempts",current.optInt("conflictAttempts",0)+1).put("state","pending").remove("nextAttemptAt");updateMutation(context,db,config.namespace,sent.id,current);continue;}if(!"retryable".equals(rejection.optString("kind"))||sent.attempts>=config.maxAttempts)current.put("state","dead-letter").put("deadLetteredAt",System.currentTimeMillis()).remove("nextAttemptAt");else current.put("state","pending").put("nextAttemptAt",System.currentTimeMillis()+Math.max(0,rejection.optLong("retryAfterMs",Math.min(30000,500L<<Math.min(20,sent.attempts-1)))));updateMutation(context,db,config.namespace,sent.id,current);}
|
|
83
|
+
for(Map.Entry<String,JSONObject> requested:b.pulls.entrySet()){JSONObject result=pullResults.get(requested.getKey());if(result==null||"error".equals(result.optString("type")))continue;JSONObject current=record(context,db,"absolute_sync_collections","collection_key","collection",config.namespace,requested.getKey());if(!same(current,requested.getValue()))continue;ContentValues values=new ContentValues();JSONObject next=applyPull(current,result);values.put("record_json",encodeRecord(context,next,"collection",config.namespace,next.optString("collection",requested.getKey())));db.update("absolute_sync_collections",values,"namespace=? AND collection_key=?",new String[]{config.namespace,requested.getKey()});pulled++;}
|
|
77
84
|
db.setTransactionSuccessful();
|
|
78
85
|
}finally{db.endTransaction();}return new int[]{ack,pulled};
|
|
79
86
|
}
|
|
@@ -83,8 +90,12 @@ final class AbsoluteBackgroundSyncEngine {
|
|
|
83
90
|
private static void remove(Map<String,JSONObject> rows,JSONArray values)throws Exception{for(int i=0;i<values.length();i++)rows.remove(key(values.getJSONObject(i)));}
|
|
84
91
|
private static boolean same(JSONObject a,JSONObject b){return a!=null&&a.optInt("version")==b.optInt("version")&&a.optString("cursor").equals(b.optString("cursor"));}
|
|
85
92
|
private static Map<String,JSONObject> index(JSONArray values,String field)throws Exception{Map<String,JSONObject> result=new HashMap<>();for(int i=0;i<values.length();i++){JSONObject value=values.getJSONObject(i);result.put(value.getString(field),value);}return result;}
|
|
86
|
-
private static JSONObject record(SQLiteDatabase db,String table,String field,String namespace,String id)throws Exception{try(Cursor c=db.rawQuery("SELECT record_json FROM "+table+" WHERE namespace=? AND "+field+"=? LIMIT 1",new String[]{namespace,id})){return c.moveToFirst()?
|
|
87
|
-
private static void updateMutation(SQLiteDatabase db,String namespace,String id,JSONObject record){ContentValues values=new ContentValues();values.put("record_json",record.
|
|
93
|
+
private static JSONObject record(Context context,SQLiteDatabase db,String table,String field,String kind,String namespace,String id)throws Exception{try(Cursor c=db.rawQuery("SELECT record_json FROM "+table+" WHERE namespace=? AND "+field+"=? LIMIT 1",new String[]{namespace,id})){return c.moveToFirst()?decodeRecord(context,c.getString(0),kind,namespace):null;}}
|
|
94
|
+
private static void updateMutation(Context context,SQLiteDatabase db,String namespace,String id,JSONObject record)throws Exception{ContentValues values=new ContentValues();values.put("record_json",encodeRecord(context,record,"mutation",namespace,record.getString("name")));db.update("absolute_sync_mutations",values,"namespace=? AND operation_id=?",new String[]{namespace,id});}
|
|
95
|
+
private static byte[] dataKey(Context context)throws Exception{String value=AbsoluteSecureStorageVault.get(context,DATA_KEY);if(value==null)return null;byte[] key=Base64.decode(value,Base64.NO_WRAP);if(key.length!=32)throw new IllegalStateException("Protected Sync data key is invalid.");return key;}
|
|
96
|
+
private static String aad(String kind,String namespace,String name){return "absolute-sync-v1\u0000"+kind+"\u0000"+namespace+"\u0000"+name;}
|
|
97
|
+
private static JSONObject decodeRecord(Context context,String stored,String kind,String namespace)throws Exception{JSONObject outer=new JSONObject(stored);if(!outer.has("__absoluteSyncProtected"))return outer;JSONObject envelope=outer.getJSONObject("__absoluteSyncProtected");if(!"aes-256-gcm-v1".equals(envelope.getString("protector")))throw new IllegalStateException("Unsupported Sync data protector.");byte[] key=dataKey(context);if(key==null)throw new IllegalStateException("Protected Sync data key is unavailable.");byte[] packed=Base64.decode(envelope.getString("value"),Base64.NO_WRAP),nonce=Arrays.copyOfRange(packed,0,12),ciphertext=Arrays.copyOfRange(packed,12,packed.length);Cipher cipher=Cipher.getInstance("AES/GCM/NoPadding");cipher.init(Cipher.DECRYPT_MODE,new SecretKeySpec(key,"AES"),new GCMParameterSpec(128,nonce));cipher.updateAAD(aad(kind,namespace,envelope.getString("name")).getBytes(StandardCharsets.UTF_8));return new JSONObject(new String(cipher.doFinal(ciphertext),StandardCharsets.UTF_8));}
|
|
98
|
+
private static String encodeRecord(Context context,JSONObject record,String kind,String namespace,String name)throws Exception{byte[] key=dataKey(context);if(key==null)return record.toString();byte[] nonce=new byte[12];new SecureRandom().nextBytes(nonce);Cipher cipher=Cipher.getInstance("AES/GCM/NoPadding");cipher.init(Cipher.ENCRYPT_MODE,new SecretKeySpec(key,"AES"),new GCMParameterSpec(128,nonce));cipher.updateAAD(aad(kind,namespace,name).getBytes(StandardCharsets.UTF_8));byte[] ciphertext=cipher.doFinal(record.toString().getBytes(StandardCharsets.UTF_8)),packed=new byte[nonce.length+ciphertext.length];System.arraycopy(nonce,0,packed,0,nonce.length);System.arraycopy(ciphertext,0,packed,nonce.length,ciphertext.length);return new JSONObject().put("__absoluteSyncProtected",new JSONObject().put("name",name).put("protector","aes-256-gcm-v1").put("value",Base64.encodeToString(packed,Base64.NO_WRAP))).toString();}
|
|
88
99
|
|
|
89
100
|
private static JSONObject request(String address,String method,byte[] body,String type,String auth,String trustedOrigin)throws Exception{return request(address,method,body,type,auth,trustedOrigin,false);}
|
|
90
101
|
private static JSONObject request(String address,String method,byte[] body,String type,String auth,String trustedOrigin,boolean advertisedTokenEndpoint)throws Exception{
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { type SQLiteDBConnection } from '@capacitor-community/sqlite';
|
|
2
|
-
import type { SyncLocalStore, SyncLocalStoreSchemaInput } from '@absolutejs/sync/client';
|
|
2
|
+
import type { SyncLocalStore, SyncLocalStoreSchemaInput, SyncLocalProtectionProvider } from '@absolutejs/sync/client';
|
|
3
3
|
import type { DeviceLifecycleCapability, DeviceNetworkCapability, DeviceSubscription } from '@absolutejs/devices';
|
|
4
4
|
import type { SyncClient } from '@absolutejs/sync/client';
|
|
5
|
+
import type { DeviceSecureStorageCapability } from '@absolutejs/devices';
|
|
6
|
+
export type CapacitorSyncProtectionOptions = {
|
|
7
|
+
secureStorage?: DeviceSecureStorageCapability;
|
|
8
|
+
};
|
|
9
|
+
/** AES-256-GCM record protection whose key is sealed by Keychain/Keystore. */
|
|
10
|
+
export declare const createCapacitorSyncProtection: (options?: CapacitorSyncProtectionOptions) => SyncLocalProtectionProvider;
|
|
5
11
|
export type CapacitorBackgroundSyncConfig = {
|
|
6
12
|
endpoint: string;
|
|
7
13
|
issuer: string;
|
|
@@ -39,13 +45,15 @@ export type CapacitorSyncLocalStoreOptions = {
|
|
|
39
45
|
connection?: CapacitorSyncSqliteFactory;
|
|
40
46
|
/** Same generated logical migration plan used by web IndexedDB. */
|
|
41
47
|
storageSchema?: SyncLocalStoreSchemaInput;
|
|
48
|
+
protection?: SyncLocalProtectionProvider;
|
|
49
|
+
now?: () => number;
|
|
42
50
|
};
|
|
43
51
|
/**
|
|
44
52
|
* Native SQLite implementation of Sync's principal-partitioned atomic cache
|
|
45
53
|
* and mutation outbox. All calls are serialized because a Capacitor database
|
|
46
54
|
* connection owns one explicit transaction at a time.
|
|
47
55
|
*/
|
|
48
|
-
export declare const createCapacitorSyncLocalStore: ({ databaseName, connection: createConnection, storageSchema }?: CapacitorSyncLocalStoreOptions) => SyncLocalStore;
|
|
56
|
+
export declare const createCapacitorSyncLocalStore: ({ databaseName, connection: createConnection, storageSchema, protection, now }?: CapacitorSyncLocalStoreOptions) => SyncLocalStore;
|
|
49
57
|
export type CapacitorSyncLifecycleOptions = {
|
|
50
58
|
client: Pick<SyncClient, 'reconnect'> & Partial<Pick<SyncClient, 'flush'>>;
|
|
51
59
|
lifecycle: DeviceLifecycleCapability;
|