@glance-apps/sync 1.1.0 → 1.1.2

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,28 @@ 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
+
22
+ ## [1.1.1] - 2026-06-03
23
+
24
+ ### Fixed
25
+
26
+ - **WebDAV — 405 on MKCOL treated as directory-already-exists**: `mkcolWithParents`
27
+ now handles HTTP 405 (Method Not Allowed) the same as 409/404, triggering the
28
+ parent-directory creation fallback. Some WebDAV servers (e.g. certain nginx
29
+ configurations) return 405 rather than 409 when MKCOL is called on a path that
30
+ already exists as a collection.
31
+
10
32
  ## [1.1.0] - 2026-05-25
11
33
 
12
34
  ### Added
@@ -97,7 +119,10 @@ entire sync infrastructure can be shared across the GLANCE app family.
97
119
  - TypeScript declarations in `types/index.d.ts`.
98
120
  - WebDAV CORS proxy handler in `api/webdav-proxy.js`.
99
121
 
100
- [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.0.3...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
124
+ [1.1.1]: https://github.com/glance-apps/glance-sync/compare/v1.1.0...v1.1.1
125
+ [1.1.0]: https://github.com/glance-apps/glance-sync/compare/v1.0.3...v1.1.0
101
126
  [1.0.3]: https://github.com/glance-apps/glance-sync/compare/v1.0.2...v1.0.3
102
127
  [1.0.2]: https://github.com/glance-apps/glance-sync/compare/v1.0.1...v1.0.2
103
128
  [1.0.1]: https://github.com/glance-apps/glance-sync/compare/v1.0.0...v1.0.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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
@@ -102,7 +102,7 @@ async function parseDownloadResponse(res, engineConfig) {
102
102
 
103
103
  async function mkcolWithParents(dirUrl, authHeaders, wf) {
104
104
  const res = await wf('MKCOL', dirUrl, authHeaders);
105
- if (res.status === 409 || res.status === 404) {
105
+ if (res.status === 409 || res.status === 404 || res.status === 405) {
106
106
  // Derive parent by stripping the last path segment via URL() to avoid
107
107
  // producing malformed strings on root-level inputs.
108
108
  let parent;