@glance-apps/sync 1.1.1 → 1.1.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.2] - 2026-06-04
11
+
12
+ ### Fixed
13
+
14
+ - **412 retry — local DB mutated before upload**: `applyPayload` was called before
15
+ `upload()` in `doCycle()`. If the upload returned a 412 Precondition Failed (concurrent
16
+ write from another device), the retry cycle re-merged already-dirtied local state with
17
+ the freshly downloaded remote, producing inconsistent data (e.g. completion events
18
+ referencing chore sync IDs that no longer exist after the second merge). `applyPayload`
19
+ is now deferred until after a successful upload (204), leaving the local DB untouched
20
+ if the upload fails so the retry starts from a clean state.
21
+
10
22
  ## [1.1.1] - 2026-06-03
11
23
 
12
24
  ### Fixed
@@ -107,7 +119,8 @@ entire sync infrastructure can be shared across the GLANCE app family.
107
119
  - TypeScript declarations in `types/index.d.ts`.
108
120
  - WebDAV CORS proxy handler in `api/webdav-proxy.js`.
109
121
 
110
- [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.1.1...HEAD
122
+ [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.1.2...HEAD
123
+ [1.1.2]: https://github.com/glance-apps/glance-sync/compare/v1.1.1...v1.1.2
111
124
  [1.1.1]: https://github.com/glance-apps/glance-sync/compare/v1.1.0...v1.1.1
112
125
  [1.1.0]: https://github.com/glance-apps/glance-sync/compare/v1.0.3...v1.1.0
113
126
  [1.0.3]: https://github.com/glance-apps/glance-sync/compare/v1.0.2...v1.0.3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/engine.js CHANGED
@@ -346,34 +346,36 @@ export const createSyncEngine = (config) => {
346
346
  const mergeResult = mergePayloads(localData, remote.data);
347
347
  const { data: mergedData, localChanged, remoteChanged } = mergeResult;
348
348
 
349
- if (localChanged) {
350
- let mayApply = true;
351
- if (validateApplyPayload) {
352
- const check = await validateApplyPayload({ ...remote, data: mergedData });
353
- if (!check?.valid) {
354
- // eslint-disable-next-line no-console
355
- console.error(`[${appId}] applyRemoteData aborted:`, check?.reason || 'validation failed');
356
- mayApply = false;
357
- }
358
- }
359
- if (mayApply) {
360
- await applyPayload(mergedData, { allowEmpty: !!remote.lastModified });
361
- localStorage.setItem(KEY_LOCAL_MOD, new Date().toISOString());
362
- }
363
- }
364
-
365
349
  if (remoteChanged || localChanged) {
366
350
  const mergedPayload = {
367
351
  version: ENVELOPE_DATA_VERSION,
368
352
  lastModified: new Date().toISOString(),
369
353
  data: mergedData,
370
354
  };
355
+ // Upload first — if this throws (e.g. 412 PRECONDITION_FAILED), the
356
+ // local DB must remain untouched so the retry starts from clean state.
371
357
  await upload({
372
358
  prebuiltPayload: mergedPayload,
373
359
  skipLockCheck: true,
374
360
  etag: etag || overrideEtag || null,
375
361
  });
376
362
 
363
+ if (localChanged) {
364
+ let mayApply = true;
365
+ if (validateApplyPayload) {
366
+ const check = await validateApplyPayload({ ...remote, data: mergedData });
367
+ if (!check?.valid) {
368
+ // eslint-disable-next-line no-console
369
+ console.error(`[${appId}] applyRemoteData aborted:`, check?.reason || 'validation failed');
370
+ mayApply = false;
371
+ }
372
+ }
373
+ if (mayApply) {
374
+ await applyPayload(mergedData, { allowEmpty: !!remote.lastModified });
375
+ localStorage.setItem(KEY_LOCAL_MOD, new Date().toISOString());
376
+ }
377
+ }
378
+
377
379
  if (hasNeverSynced && localChanged) {
378
380
  onFirstSyncReload?.();
379
381
  return true;
package/src/providers.js CHANGED
@@ -246,7 +246,12 @@ export function createProviders(engineConfig) {
246
246
  // nativeHttpRequest != null means the Android HTTP bridge is available,
247
247
  // which bypasses Koofr's server-side block on Vercel proxy IPs.
248
248
  const onAndroid = engineConfig.nativeHttpRequest != null;
249
- if (!onAndroid) return { success: false, error: "Koofr is blocking requests from the web app's server. Use the Android app — it connects to Koofr directly." };
249
+ if (!onAndroid) {
250
+ if (engineConfig.isHostedApp) {
251
+ return { success: false, error: "Koofr blocks requests from dayglance.app's servers. Use a self-hosted instance or the Android app to connect directly." };
252
+ }
253
+ return { success: false, error: "Access forbidden. If requests are going through a proxy, Koofr may be blocking that server's IP." };
254
+ }
250
255
  return { success: false, error: 'Access forbidden. Check that your app password is correct and has not been revoked.' };
251
256
  }
252
257
  return { success: false, error: `Unexpected response: ${res.status}${res.statusText ? ' ' + res.statusText : ''}` };