@pylonsync/sync 0.3.222 → 0.3.224
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/package.json +1 -1
- package/src/index.ts +63 -10
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1976,21 +1976,23 @@ export class SyncEngine {
|
|
|
1976
1976
|
typeof r.error === "string"
|
|
1977
1977
|
? r.error
|
|
1978
1978
|
: r.error?.message ?? "unknown";
|
|
1979
|
-
this.
|
|
1979
|
+
this.failPushedMutation(m, msg);
|
|
1980
1980
|
}
|
|
1981
1981
|
}
|
|
1982
1982
|
} else {
|
|
1983
1983
|
// Legacy server response (pre-0.3.188): count-based mapping.
|
|
1984
1984
|
// Buggy on partial failures but the best we can do without
|
|
1985
|
-
// the per-op envelope.
|
|
1985
|
+
// the per-op envelope. Guard `resp.errors` — older test
|
|
1986
|
+
// mocks omit the field entirely; pre-0.3.224 the `[]` index
|
|
1987
|
+
// threw and was swallowed by the bare catch below, which
|
|
1988
|
+
// silently dropped the success path for legacy responses.
|
|
1989
|
+
const applied = typeof resp.applied === "number" ? resp.applied : 0;
|
|
1990
|
+
const errors = Array.isArray(resp.errors) ? resp.errors : [];
|
|
1986
1991
|
for (let i = 0; i < pending.length; i++) {
|
|
1987
|
-
if (i <
|
|
1992
|
+
if (i < applied) {
|
|
1988
1993
|
this.mutations.markApplied(pending[i].id);
|
|
1989
|
-
} else if (
|
|
1990
|
-
this.
|
|
1991
|
-
pending[i].id,
|
|
1992
|
-
resp.errors[i - resp.applied],
|
|
1993
|
-
);
|
|
1994
|
+
} else if (errors[i - applied]) {
|
|
1995
|
+
this.failPushedMutation(pending[i], errors[i - applied]);
|
|
1994
1996
|
}
|
|
1995
1997
|
}
|
|
1996
1998
|
}
|
|
@@ -2046,9 +2048,60 @@ export class SyncEngine {
|
|
|
2046
2048
|
void this.push();
|
|
2047
2049
|
}, 250);
|
|
2048
2050
|
}
|
|
2049
|
-
} catch {
|
|
2050
|
-
//
|
|
2051
|
+
} catch (err) {
|
|
2052
|
+
// Transport-level failure (network down, CORS, 5xx without a
|
|
2053
|
+
// typed body, parse error). Pre-0.3.224 swallowed silently:
|
|
2054
|
+
// the mutation stayed `pending` forever and the optimistic
|
|
2055
|
+
// ghost survived even though the server never accepted the
|
|
2056
|
+
// write. That's the "I sent it, it's there, then it's gone"
|
|
2057
|
+
// pattern users see after a reload.
|
|
2058
|
+
//
|
|
2059
|
+
// Now: fail every pending mutation in this batch, roll back
|
|
2060
|
+
// any optimistic ghost, surface via mutations-failed so the
|
|
2061
|
+
// UI can prompt + retry. op_id keeps a retry idempotent on
|
|
2062
|
+
// the server if the failure was a transient transport error
|
|
2063
|
+
// — the next push() will re-include the user's intent.
|
|
2064
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2065
|
+
const failedOps: { opId: string; error: string }[] = [];
|
|
2066
|
+
for (const m of pending) {
|
|
2067
|
+
this.failPushedMutation(m, msg);
|
|
2068
|
+
const opId = m.change.op_id;
|
|
2069
|
+
if (typeof opId === "string") {
|
|
2070
|
+
failedOps.push({ opId, error: msg });
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
if (failedOps.length > 0) {
|
|
2074
|
+
this.broadcastToTabs({ type: "mutations-failed", ops: failedOps });
|
|
2075
|
+
}
|
|
2076
|
+
this.mutations.clear();
|
|
2077
|
+
// eslint-disable-next-line no-console
|
|
2078
|
+
console.warn("[sync] /api/sync/push failed:", msg);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* Mark a pending mutation as failed AND undo its optimistic ghost
|
|
2084
|
+
* in the local replica. Without the rollback step, a server-
|
|
2085
|
+
* rejected insert leaves a ghost row that survives indefinitely
|
|
2086
|
+
* (reconcile skips rows with pending/failed mutations to avoid
|
|
2087
|
+
* sweeping the user's in-flight edit). The exact failure mode is
|
|
2088
|
+
* "send a message, the server says no, refresh — the ghost is
|
|
2089
|
+
* still there until you find the failed-state UI."
|
|
2090
|
+
*
|
|
2091
|
+
* Updates can't be rolled back without a pre-update snapshot
|
|
2092
|
+
* (not captured today); the user-visible ghost-update sticks
|
|
2093
|
+
* until the next reconcile observes the canonical row. Deletes
|
|
2094
|
+
* leave a tombstone that should also be cleared, but the current
|
|
2095
|
+
* `LocalStore` API doesn't expose the un-tombstone path — flagged
|
|
2096
|
+
* for follow-up. Inserts are the dominant case (chat send is an
|
|
2097
|
+
* insert; collaborative-edit is an update with a separate CRDT
|
|
2098
|
+
* channel) so insert-only rollback is the right shape to ship now.
|
|
2099
|
+
*/
|
|
2100
|
+
private failPushedMutation(m: PendingMutation, error: string): void {
|
|
2101
|
+
if (m.change.kind === "insert") {
|
|
2102
|
+
this.store.rollbackOptimisticInsert(m.change.entity, m.change.row_id);
|
|
2051
2103
|
}
|
|
2104
|
+
this.mutations.markFailed(m.id, error);
|
|
2052
2105
|
}
|
|
2053
2106
|
|
|
2054
2107
|
/** Insert a row with optimistic local update.
|