cry-synced-db-client 0.1.166 → 0.1.168

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/dist/index.js CHANGED
@@ -329,6 +329,22 @@ function sameIdSequence(a, b) {
329
329
  }
330
330
  return true;
331
331
  }
332
+ function containsIdArrayDescendant(value) {
333
+ if (value === null || typeof value !== "object") return false;
334
+ if (value instanceof Date || isObjectIdLike(value)) return false;
335
+ if (Array.isArray(value)) {
336
+ if (value.length > 0 && allElementsHaveId(value)) return true;
337
+ for (const v of value) {
338
+ if (containsIdArrayDescendant(v)) return true;
339
+ }
340
+ return false;
341
+ }
342
+ if (!isPlainObject(value)) return false;
343
+ for (const key of Object.keys(value)) {
344
+ if (containsIdArrayDescendant(value[key])) return true;
345
+ }
346
+ return false;
347
+ }
332
348
  function computeArrayDiff(existingArr, updateArr, basePath, diff) {
333
349
  if (existingArr.length === 0 && updateArr.length === 0) return;
334
350
  if (!allElementsHaveId(updateArr) || existingArr.length > 0 && !allElementsHaveId(existingArr)) {
@@ -354,12 +370,14 @@ function computeArrayDiff(existingArr, updateArr, basePath, diff) {
354
370
  if (sameIdSequence(existingArr, updateArr)) {
355
371
  for (let i = 0; i < updateArr.length; i++) {
356
372
  const elementId = String(updateArr[i]._id);
357
- computeDiffInto(
358
- existingArr[i],
359
- updateArr[i],
360
- `${basePath}[${elementId}]`,
361
- diff
362
- );
373
+ const elementPath = `${basePath}[${elementId}]`;
374
+ if (containsIdArrayDescendant(updateArr[i])) {
375
+ if (!deepEquals(existingArr[i], updateArr[i])) {
376
+ diff[elementPath] = updateArr[i];
377
+ }
378
+ } else {
379
+ computeDiffInto(existingArr[i], updateArr[i], elementPath, diff);
380
+ }
363
381
  }
364
382
  } else {
365
383
  diff[basePath] = updateArr;
@@ -383,12 +401,19 @@ function computeArrayDiff(existingArr, updateArr, basePath, diff) {
383
401
  for (const updateEl of updateArr) {
384
402
  const id = String(updateEl._id);
385
403
  if (existingIds.has(id)) {
386
- computeDiffInto(
387
- existingById.get(id),
388
- updateEl,
389
- `${basePath}[${id}]`,
390
- diff
391
- );
404
+ const elementPath = `${basePath}[${id}]`;
405
+ if (containsIdArrayDescendant(updateEl)) {
406
+ if (!deepEquals(existingById.get(id), updateEl)) {
407
+ diff[elementPath] = updateEl;
408
+ }
409
+ } else {
410
+ computeDiffInto(
411
+ existingById.get(id),
412
+ updateEl,
413
+ elementPath,
414
+ diff
415
+ );
416
+ }
392
417
  }
393
418
  }
394
419
  }
@@ -3271,11 +3296,45 @@ var _SyncEngine = class _SyncEngine {
3271
3296
  let sentCount = 0;
3272
3297
  const collectionSentCounts = {};
3273
3298
  for (const result of results) {
3274
- const { collection, results: { inserted, updated, deleted, errors: errors2 } } = result;
3299
+ const {
3300
+ collection,
3301
+ results: { inserted, updated, deleted, errors: errors2, warnings }
3302
+ } = result;
3303
+ const erroredIds = /* @__PURE__ */ new Set();
3304
+ if (errors2 && errors2.length > 0) {
3305
+ for (const e of errors2) erroredIds.add(String(e._id));
3306
+ }
3275
3307
  const allSuccessIds = [];
3276
- for (const e of inserted) allSuccessIds.push(e._id);
3277
- for (const e of updated) allSuccessIds.push(e._id);
3278
- for (const e of deleted) allSuccessIds.push(e._id);
3308
+ const ambiguous = [];
3309
+ for (const e of inserted) {
3310
+ const sid = String(e._id);
3311
+ if (erroredIds.has(sid)) {
3312
+ ambiguous.push(sid);
3313
+ continue;
3314
+ }
3315
+ allSuccessIds.push(e._id);
3316
+ }
3317
+ for (const e of updated) {
3318
+ const sid = String(e._id);
3319
+ if (erroredIds.has(sid)) {
3320
+ ambiguous.push(sid);
3321
+ continue;
3322
+ }
3323
+ allSuccessIds.push(e._id);
3324
+ }
3325
+ for (const e of deleted) {
3326
+ const sid = String(e._id);
3327
+ if (erroredIds.has(sid)) {
3328
+ ambiguous.push(sid);
3329
+ continue;
3330
+ }
3331
+ allSuccessIds.push(e._id);
3332
+ }
3333
+ if (ambiguous.length > 0) {
3334
+ console.error(
3335
+ `Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
3336
+ );
3337
+ }
3279
3338
  if (allSuccessIds.length > 0) {
3280
3339
  await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
3281
3340
  }
@@ -3368,12 +3427,19 @@ var _SyncEngine = class _SyncEngine {
3368
3427
  });
3369
3428
  }
3370
3429
  }
3371
- if (errors2) {
3372
- console.error(
3373
- `Sync upload errors for ${collection}:`,
3374
- errors2,
3375
- "\u2014 dirty entries for failed items will persist until retry"
3376
- );
3430
+ if (errors2 && errors2.length > 0) {
3431
+ for (const e of errors2) {
3432
+ console.error(
3433
+ `Sync upload error [${collection}] _id=${e._id}: ${e.error} \u2014 dirty entry will persist until retry`
3434
+ );
3435
+ }
3436
+ }
3437
+ if (warnings && warnings.length > 0) {
3438
+ for (const w of warnings) {
3439
+ console.warn(
3440
+ `Sync upload warning [${collection}] _id=${w._id}: ${w.error}`
3441
+ );
3442
+ }
3377
3443
  }
3378
3444
  const sentIds = /* @__PURE__ */ new Set([
3379
3445
  ...collectionBatches.flat().filter((b) => b.collection === collection).flatMap((b) => [
@@ -3433,15 +3499,60 @@ var _SyncEngine = class _SyncEngine {
3433
3499
  );
3434
3500
  let sentCount = 0;
3435
3501
  for (const result of results) {
3436
- const { results: { inserted, updated, deleted } } = result;
3502
+ const {
3503
+ results: { inserted, updated, deleted, errors: errors2, warnings }
3504
+ } = result;
3505
+ const erroredIds = /* @__PURE__ */ new Set();
3506
+ if (errors2 && errors2.length > 0) {
3507
+ for (const e of errors2) {
3508
+ erroredIds.add(String(e._id));
3509
+ console.error(
3510
+ `Sync upload error [${collection}] _id=${e._id}: ${e.error} \u2014 dirty entry will persist until retry`
3511
+ );
3512
+ }
3513
+ }
3514
+ if (warnings && warnings.length > 0) {
3515
+ for (const w of warnings) {
3516
+ console.warn(
3517
+ `Sync upload warning [${collection}] _id=${w._id}: ${w.error}`
3518
+ );
3519
+ }
3520
+ }
3437
3521
  const allSuccessIds = [];
3438
- for (const e of inserted) allSuccessIds.push(e._id);
3439
- for (const e of updated) allSuccessIds.push(e._id);
3440
- for (const e of deleted) allSuccessIds.push(e._id);
3522
+ const ambiguous = [];
3523
+ for (const e of inserted) {
3524
+ const sid = String(e._id);
3525
+ if (erroredIds.has(sid)) {
3526
+ ambiguous.push(sid);
3527
+ continue;
3528
+ }
3529
+ allSuccessIds.push(e._id);
3530
+ }
3531
+ for (const e of updated) {
3532
+ const sid = String(e._id);
3533
+ if (erroredIds.has(sid)) {
3534
+ ambiguous.push(sid);
3535
+ continue;
3536
+ }
3537
+ allSuccessIds.push(e._id);
3538
+ }
3539
+ for (const e of deleted) {
3540
+ const sid = String(e._id);
3541
+ if (erroredIds.has(sid)) {
3542
+ ambiguous.push(sid);
3543
+ continue;
3544
+ }
3545
+ allSuccessIds.push(e._id);
3546
+ }
3547
+ if (ambiguous.length > 0) {
3548
+ console.error(
3549
+ `Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
3550
+ );
3551
+ }
3441
3552
  if (allSuccessIds.length > 0) {
3442
3553
  await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
3443
3554
  }
3444
- sentCount += inserted.length + updated.length + deleted.length;
3555
+ sentCount += allSuccessIds.length;
3445
3556
  }
3446
3557
  return { sentCount };
3447
3558
  }
@@ -4963,7 +5074,8 @@ var _SyncedDb = class _SyncedDb {
4963
5074
  const merged = _SyncedDb.applyDiffLocally(
4964
5075
  currentMem != null ? currentMem : existing,
4965
5076
  diff,
4966
- id
5077
+ id,
5078
+ collection
4967
5079
  );
4968
5080
  this.pendingChanges.schedule(collection, id, merged, 0, "save");
4969
5081
  if (!isWriteOnly && !(existing == null ? void 0 : existing._deleted) && !(existing == null ? void 0 : existing._archived)) {
@@ -6122,7 +6234,7 @@ var _SyncedDb = class _SyncedDb {
6122
6234
  * Returns a new object (the cloned-and-mutated `base`); never mutates
6123
6235
  * the input `base` reference.
6124
6236
  */
6125
- static applyDiffLocally(base, diff, fallbackId) {
6237
+ static applyDiffLocally(base, diff, fallbackId, collection) {
6126
6238
  const seed = base ? _SyncedDb.safeDeepClone(base) : { _id: fallbackId };
6127
6239
  if (seed._id == null) seed._id = fallbackId;
6128
6240
  for (const path of Object.keys(diff)) {
@@ -6136,10 +6248,98 @@ var _SyncedDb = class _SyncedDb {
6136
6248
  continue;
6137
6249
  }
6138
6250
  const ok = setByPath(seed, path, value);
6139
- if (!ok) seed[path] = value;
6251
+ if (!ok) {
6252
+ _SyncedDb.materializeBracketPath(seed, path, value, collection, fallbackId);
6253
+ }
6140
6254
  }
6141
6255
  return seed;
6142
6256
  }
6257
+ /**
6258
+ * Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
6259
+ * the parent array when it's missing from `seed`. Two patterns covered, per
6260
+ * production-spec (mozirje 2026-05-10 — literal bracket-keyed sibling
6261
+ * properties polluting Dexie/in-mem):
6262
+ *
6263
+ * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
6264
+ * 2. `polje[<id>].<field> = <v>` → seed.polje = [{_id: <id>, <field>: <v>}]
6265
+ *
6266
+ * Everything else (nested-via-dots before bracket, multi-bracket paths
6267
+ * like `_redundanca.terapije[<id>].postavke[<id2>]`, deeper sub-fields)
6268
+ * is dropped silently — materializing those locally would risk corrupting
6269
+ * unrelated invariants on the nested objects. Dirty-change still carries
6270
+ * the path, so server applies it; next sync brings the canonical state
6271
+ * back to local.
6272
+ *
6273
+ * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
6274
+ * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
6275
+ * onto Dexie rows and in-mem state, persisting forever through subsequent
6276
+ * `safeDeepClone`-based save cycles.
6277
+ */
6278
+ static materializeBracketPath(seed, path, value, collection, id) {
6279
+ const tokens = tokenizePath(path);
6280
+ const firstToken = tokens[0];
6281
+ const dropSilently = typeof firstToken === "string" && firstToken.startsWith("_");
6282
+ const drop = (reason) => {
6283
+ if (dropSilently) return;
6284
+ console.error(
6285
+ `SyncedDb.applyDiffLocally: dropping bracket-path diff entry (${reason})`,
6286
+ { collection, _id: String(id), path, value }
6287
+ );
6288
+ };
6289
+ if (tokens.length < 2 || tokens.length > 3) {
6290
+ drop(`unsupported token count ${tokens.length}`);
6291
+ return;
6292
+ }
6293
+ if (firstToken === void 0 || firstToken.startsWith("[")) {
6294
+ drop("first segment is not a plain field");
6295
+ return;
6296
+ }
6297
+ const secondToken = tokens[1];
6298
+ if (!secondToken.startsWith("[") || !secondToken.endsWith("]")) {
6299
+ drop("second segment is not a bracket-by-id");
6300
+ return;
6301
+ }
6302
+ if (tokens.length === 3 && tokens[2].startsWith("[")) {
6303
+ drop("nested bracket path");
6304
+ return;
6305
+ }
6306
+ const bracketId = secondToken.slice(1, -1);
6307
+ if (bracketId.length === 0) {
6308
+ drop("empty bracket id");
6309
+ return;
6310
+ }
6311
+ const existing = seed[firstToken];
6312
+ if (existing != null && !Array.isArray(existing)) {
6313
+ drop(`existing "${firstToken}" is not an array`);
6314
+ return;
6315
+ }
6316
+ if (tokens.length === 2) {
6317
+ let element = value;
6318
+ if (Array.isArray(value) && value.length === 1 && value[0] != null && typeof value[0] === "object") {
6319
+ element = value[0];
6320
+ }
6321
+ if (element == null || typeof element !== "object" || Array.isArray(element)) {
6322
+ drop("value is not a single element or wire-form wrapper");
6323
+ return;
6324
+ }
6325
+ if (element._id == null) {
6326
+ element._id = bracketId;
6327
+ }
6328
+ if (existing == null) {
6329
+ seed[firstToken] = [element];
6330
+ } else {
6331
+ existing.push(element);
6332
+ }
6333
+ return;
6334
+ }
6335
+ const fieldName = tokens[2];
6336
+ const newElement = { _id: bracketId, [fieldName]: value };
6337
+ if (existing == null) {
6338
+ seed[firstToken] = [newElement];
6339
+ } else {
6340
+ existing.push(newElement);
6341
+ }
6342
+ }
6143
6343
  /**
6144
6344
  * Deep clone for `applyDiffLocally`. Recurses into plain objects and
6145
6345
  * arrays; preserves `Date` (cloned to avoid shared reference) and
@@ -43,7 +43,6 @@ export interface RestProxyConfig {
43
43
  * ## Performance
44
44
  *
45
45
  * Benchmarked at ~5ms per request (after warmup) on localhost.
46
- * Native fetch performs comparably to undici with connection pooling.
47
46
  *
48
47
  * ## Progress Callback Limitations
49
48
  *
@@ -435,6 +435,28 @@ export declare class SyncedDb implements I_SyncedDb {
435
435
  * the input `base` reference.
436
436
  */
437
437
  private static applyDiffLocally;
438
+ /**
439
+ * Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
440
+ * the parent array when it's missing from `seed`. Two patterns covered, per
441
+ * production-spec (mozirje 2026-05-10 — literal bracket-keyed sibling
442
+ * properties polluting Dexie/in-mem):
443
+ *
444
+ * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
445
+ * 2. `polje[<id>].<field> = <v>` → seed.polje = [{_id: <id>, <field>: <v>}]
446
+ *
447
+ * Everything else (nested-via-dots before bracket, multi-bracket paths
448
+ * like `_redundanca.terapije[<id>].postavke[<id2>]`, deeper sub-fields)
449
+ * is dropped silently — materializing those locally would risk corrupting
450
+ * unrelated invariants on the nested objects. Dirty-change still carries
451
+ * the path, so server applies it; next sync brings the canonical state
452
+ * back to local.
453
+ *
454
+ * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
455
+ * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
456
+ * onto Dexie rows and in-mem state, persisting forever through subsequent
457
+ * `safeDeepClone`-based save cycles.
458
+ */
459
+ private static materializeBracketPath;
438
460
  /**
439
461
  * Deep clone for `applyDiffLocally`. Recurses into plain objects and
440
462
  * arrays; preserves `Date` (cloned to avoid shared reference) and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.166",
3
+ "version": "0.1.168",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "clean": "rm -rf dist",
20
20
  "build": "bun run clean && bun run build:js && bun run build:types",
21
- "build:js": "esbuild ./src/index.ts --bundle --outdir=./dist --target=es2017 --format=esm --platform=browser --external:dexie --external:bson --external:cry-helpers --external:notepack.io",
21
+ "build:js": "esbuild ./src/index.ts --bundle --outdir=./dist --target=es2017 --format=esm --platform=browser --external:dexie --external:bson --external:cry-helpers",
22
22
  "build:types": "tsc --emitDeclarationOnly --outDir dist",
23
23
  "test": "bun test test/*.test.ts test/restProxy/*.test.ts && vitest run",
24
24
  "test:bun": "bun test test/*.test.ts test/restProxy/*.test.ts",
@@ -39,10 +39,7 @@
39
39
  "dependencies": {
40
40
  "cry-helpers": "^2.1.194",
41
41
  "msgpackr": "^2.0.1",
42
- "notepack": "^0.0.2",
43
- "notepack.io": "^3.0.1",
44
- "superjson": "^2.2.6",
45
- "undici": "^8.2.0"
42
+ "superjson": "^2.2.6"
46
43
  },
47
44
  "peerDependencies": {
48
45
  "bson": "^6.0.0 || ^7.0.0",