@absolutejs/sync-capacitor 0.7.0 → 0.9.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
CHANGED
|
@@ -41,6 +41,14 @@ failures obey the client's delivery ceiling; conflicts and permanent failures
|
|
|
41
41
|
remain in the principal's dead-letter partition for explicit remediation rather
|
|
42
42
|
than replaying forever.
|
|
43
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
|
+
|
|
44
52
|
The namespace must come from a verified Auth principal, never from an untrusted
|
|
45
53
|
route or form value. AbsoluteJS derives an opaque namespace from the verified
|
|
46
54
|
issuer, public client ID, and subject. Signing out locks that partition by
|
|
@@ -79,7 +79,7 @@ final class AbsoluteBackgroundSyncEngine {
|
|
|
79
79
|
private static int[] settle(Context context,SQLiteDatabase db,AbsoluteBackgroundSyncConfig config,Batch b,JSONObject response)throws Exception{
|
|
80
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();
|
|
81
81
|
try{
|
|
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);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);}
|
|
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
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++;}
|
|
84
84
|
db.setTransactionSuccessful();
|
|
85
85
|
}finally{db.endTransaction();}return new int[]{ack,pulled};
|
|
@@ -97,6 +97,12 @@ enum AbsoluteBackgroundSyncEngine {
|
|
|
97
97
|
if mutationResults[sent.id]?["status"] as? String == "ack" { try execute(db, "DELETE FROM absolute_sync_mutations WHERE namespace=? AND operation_id=?", [config.namespace, sent.id]); acknowledged += 1; continue }
|
|
98
98
|
let rejection = mutationResults[sent.id]?["rejection"] as? [String: Any] ?? ["kind": "retryable", "message": "Missing mutation response."]
|
|
99
99
|
current["lastError"] = rejection["message"]; current["rejection"] = rejection
|
|
100
|
+
let conflictPolicy = current["conflictPolicy"] as? [String: Any]
|
|
101
|
+
let conflictStrategy = conflictPolicy?["strategy"] as? String ?? "manual"
|
|
102
|
+
if rejection["kind"] as? String == "conflict" && conflictStrategy == "server-wins" { try execute(db, "DELETE FROM absolute_sync_mutations WHERE namespace=? AND operation_id=?", [config.namespace, sent.id]); continue }
|
|
103
|
+
let conflictAttempts = (current["conflictAttempts"] as? NSNumber)?.intValue ?? 0
|
|
104
|
+
let maxConflictAttempts = (conflictPolicy?["maxAttempts"] as? NSNumber)?.intValue ?? 1
|
|
105
|
+
if rejection["kind"] as? String == "conflict" && conflictStrategy == "client-wins" && conflictAttempts < maxConflictAttempts { current["conflictAttempts"] = conflictAttempts + 1; current["state"] = "pending"; current.removeValue(forKey: "nextAttemptAt"); try updateRecord(db, "absolute_sync_mutations", "operation_id", "mutation", config.namespace, sent.id, current); continue }
|
|
100
106
|
if rejection["kind"] as? String != "retryable" || sent.attempts >= config.maxAttempts { current["state"] = "dead-letter"; current["deadLetteredAt"] = Date().timeIntervalSince1970 * 1000; current.removeValue(forKey: "nextAttemptAt") }
|
|
101
107
|
else { let hint = (rejection["retryAfterMs"] as? NSNumber)?.doubleValue ?? min(30_000, 500 * pow(2, Double(max(0, sent.attempts - 1)))); current["state"] = "pending"; current["nextAttemptAt"] = Date().timeIntervalSince1970 * 1000 + max(0, hint) }
|
|
102
108
|
try updateRecord(db, "absolute_sync_mutations", "operation_id", "mutation", config.namespace, sent.id, current)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/sync-capacitor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Transactional Capacitor SQLite local store and native lifecycle adapter for @absolutejs/sync",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,16 +36,16 @@
|
|
|
36
36
|
"release": "bun run format && bun run typecheck && bun test && bun run build && npm publish"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@absolutejs/devices": ">=0.0
|
|
40
|
-
"@absolutejs/devices-capacitor": ">=0.1
|
|
41
|
-
"@absolutejs/sync": ">=2.
|
|
39
|
+
"@absolutejs/devices": ">=0.2.0 <0.3.0",
|
|
40
|
+
"@absolutejs/devices-capacitor": ">=0.3.1 <0.4.0",
|
|
41
|
+
"@absolutejs/sync": ">=2.29.0 <3",
|
|
42
42
|
"@capacitor/core": ">=8 <9",
|
|
43
43
|
"@capacitor-community/sqlite": ">=8.1.1 <9"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@absolutejs/devices": "0.0
|
|
47
|
-
"@absolutejs/devices-capacitor": "0.1
|
|
48
|
-
"@absolutejs/sync": "2.
|
|
46
|
+
"@absolutejs/devices": "0.2.0",
|
|
47
|
+
"@absolutejs/devices-capacitor": "0.3.1",
|
|
48
|
+
"@absolutejs/sync": "2.29.0",
|
|
49
49
|
"@capacitor/core": "8.5.0",
|
|
50
50
|
"@capacitor-community/sqlite": "8.1.1",
|
|
51
51
|
"@types/bun": "1.3.14",
|