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 +230 -30
- package/dist/src/db/RestProxy.d.ts +0 -1
- package/dist/src/db/SyncedDb.d.ts +22 -0
- package/package.json +3 -6
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
|
-
|
|
358
|
-
|
|
359
|
-
updateArr[i]
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
updateEl
|
|
389
|
-
|
|
390
|
-
|
|
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 {
|
|
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
|
-
|
|
3277
|
-
for (const e of
|
|
3278
|
-
|
|
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
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
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 {
|
|
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
|
-
|
|
3439
|
-
for (const e of
|
|
3440
|
-
|
|
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 +=
|
|
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)
|
|
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
|
|
@@ -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.
|
|
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
|
|
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
|
-
"
|
|
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",
|