cry-synced-db-client 0.1.219 → 2.0.1
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
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 2.0.0 (2026-07-10)
|
|
4
|
+
|
|
5
|
+
### Dual-channel ebus support: `db/` + `dbbracketed/`
|
|
6
|
+
|
|
7
|
+
`Ebus2ProxyServerUpdateNotifier` now subscribes to **both** channels simultaneously:
|
|
8
|
+
|
|
9
|
+
| Channel | Payload | Use case |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `db/` | Full document object (`operation: insert/update/delete`) | Legacy; full object replication |
|
|
12
|
+
| `dbbracketed/` | Diff (`bracketedData: { "field": value, "arr[id].sub": value }`) | Bandwidth-efficient; only changed fields |
|
|
13
|
+
|
|
14
|
+
Both channels carry `pub_ts` (publisher timestamp) at batch level. The client
|
|
15
|
+
uses `pub_ts` for cross-channel dedup — if the same batch arrives on both
|
|
16
|
+
channels, only the first one is processed.
|
|
17
|
+
|
|
18
|
+
**Server-side:** cry-db ≥ 2.5.x emits both channels for every change. The
|
|
19
|
+
diff is produced by `computeObjDiff` (same function the client uses for dirty
|
|
20
|
+
changes), so the format is identical end-to-end.
|
|
21
|
+
|
|
22
|
+
**Client-side (`ServerUpdateHandler`):**
|
|
23
|
+
- `_rev`-based stale/self-echo/gap detection shared across both channels
|
|
24
|
+
- `checkRev()` returns: `ignore` | `bump-meta` | `apply` | `fetch-full`
|
|
25
|
+
- For `dbbracketed/` diffs: `applyObjDiff` from cry-db applies the diff locally
|
|
26
|
+
- `undefined` values in diffs trigger `deleteByPath` (mongo `$unset` symmetric)
|
|
27
|
+
- On gap (`incomingRev > localRev + 1`): fetches full doc from server via
|
|
28
|
+
`findById` (we're online because WS is connected); if fetch fails, fires
|
|
29
|
+
`onEbusUpdateSkipped` and skips
|
|
30
|
+
|
|
31
|
+
**Why `insert()` instead of `save()` for bracketed diffs:**
|
|
32
|
+
Dexie's `update()` only touches fields present in the payload — it does NOT
|
|
33
|
+
delete fields. When `applyObjDiff` produces a diff with `{ field: undefined }`
|
|
34
|
+
(signaling deletion via `deleteByPath`), we must use `put()` (`insert()` in our
|
|
35
|
+
interface) to replace the entire row, ensuring deleted fields are actually
|
|
36
|
+
removed locally.
|
|
37
|
+
|
|
38
|
+
**Diagnostic callbacks:**
|
|
39
|
+
```typescript
|
|
40
|
+
new SyncedDb({
|
|
41
|
+
onEbusNotificationReceived: (info) => {
|
|
42
|
+
// info: { payload, channel: "db" | "dbbracketed", collection, timestamp }
|
|
43
|
+
syslog.debug("ebus", info);
|
|
44
|
+
},
|
|
45
|
+
onEbusUpdateSkipped: (info) => {
|
|
46
|
+
// info: { collection, _id, incomingRev, localRev, reason, timestamp }
|
|
47
|
+
// reason: "fetch-failed" (gap detected but findById failed)
|
|
48
|
+
syslog.warn("ebus skip", info);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Generic `_rev` check for all server notifications
|
|
54
|
+
|
|
55
|
+
Previously only self-echoed updates (same `_lastUpdaterId`) were handled.
|
|
56
|
+
Now every incoming item runs through `checkRev()`:
|
|
57
|
+
- `incomingRev <= localRev` → ignore (stale/duplicate)
|
|
58
|
+
- `incomingRev === localRev + 1 && incomingUpdaterId === local._lastUpdaterId`
|
|
59
|
+
→ bump-meta (clear dirty, advance rev)
|
|
60
|
+
- `incomingRev === localRev + 1 && different updater` → apply (external update)
|
|
61
|
+
- `incomingRev > localRev + 1` → fetch-full (gap; online because WS is connected)
|
|
62
|
+
|
|
63
|
+
### Backward compatibility
|
|
64
|
+
|
|
65
|
+
All existing public API is unchanged. Both `db/` and `dbbracketed/` channels
|
|
66
|
+
are handled transparently. Legacy servers (cry-db < 2.5.x) that only emit
|
|
67
|
+
`db/` work identically to before.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
3
71
|
## 0.1.216 (2026-06-25)
|
|
4
72
|
|
|
5
73
|
### `onSyncProgress` fires for every chunk (not just first per collection)
|
package/dist/index.js
CHANGED
|
@@ -4444,6 +4444,8 @@ var SyncEngine = class {
|
|
|
4444
4444
|
// src/db/sync/ServerUpdateHandler.ts
|
|
4445
4445
|
var ServerUpdateHandler = class {
|
|
4446
4446
|
constructor(config) {
|
|
4447
|
+
/** Per-collection last processed pub_ts for cross-channel dedup. */
|
|
4448
|
+
this.lastProcessedPubTs = /* @__PURE__ */ new Map();
|
|
4447
4449
|
this.tenant = config.tenant;
|
|
4448
4450
|
this.updaterId = config.updaterId;
|
|
4449
4451
|
this.collections = config.collections;
|
|
@@ -4452,22 +4454,219 @@ var ServerUpdateHandler = class {
|
|
|
4452
4454
|
this.callbacks = config.callbacks;
|
|
4453
4455
|
this.deps = config.deps;
|
|
4454
4456
|
}
|
|
4457
|
+
// ============================================================
|
|
4458
|
+
// Entry point
|
|
4459
|
+
// ============================================================
|
|
4455
4460
|
/**
|
|
4456
4461
|
* Handle incoming server update (WebSocket notification).
|
|
4462
|
+
* Accepts both db/ (full-object) and dbbracketed/ (diff) payloads.
|
|
4457
4463
|
*/
|
|
4458
4464
|
async handleServerUpdate(payload) {
|
|
4465
|
+
this.callOnEbusNotificationReceived(payload);
|
|
4459
4466
|
this.callOnWsNotification(payload);
|
|
4460
4467
|
if (!this.deps.isLeader()) return;
|
|
4461
4468
|
if (!this.deps.canReceiveServerUpdates()) return;
|
|
4462
4469
|
const collectionName = payload.collection;
|
|
4463
4470
|
if (!this.collections.has(collectionName)) return;
|
|
4464
4471
|
if (!this.deps.isSyncAllowed(collectionName)) return;
|
|
4472
|
+
const pubTs = payload.pub_ts;
|
|
4473
|
+
if (this.wasAlreadyProcessed(collectionName, pubTs)) {
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
const isBracketed = this.isBracketedPayload(payload);
|
|
4477
|
+
const updatedIds = isBracketed ? await this.handleBracketedBatch(collectionName, payload) : await this.handleLegacyBatch(collectionName, payload);
|
|
4478
|
+
if (updatedIds.length > 0) {
|
|
4479
|
+
this.deps.broadcastUpdates({ [collectionName]: updatedIds });
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
// ============================================================
|
|
4483
|
+
// pub_ts dedup
|
|
4484
|
+
// ============================================================
|
|
4485
|
+
/**
|
|
4486
|
+
* Check if this batch was already processed (duplicate or stale pub_ts).
|
|
4487
|
+
* Updates lastProcessedPubTs when a new pub_ts is seen.
|
|
4488
|
+
*/
|
|
4489
|
+
wasAlreadyProcessed(collection, pubTs) {
|
|
4490
|
+
if (pubTs == null) return false;
|
|
4491
|
+
const last = this.lastProcessedPubTs.get(collection);
|
|
4492
|
+
if (last != null && pubTs <= last) {
|
|
4493
|
+
return true;
|
|
4494
|
+
}
|
|
4495
|
+
this.lastProcessedPubTs.set(collection, pubTs);
|
|
4496
|
+
return false;
|
|
4497
|
+
}
|
|
4498
|
+
isBracketedPayload(payload) {
|
|
4499
|
+
if (payload.operation !== "batch") return false;
|
|
4500
|
+
const batch = payload;
|
|
4501
|
+
return batch.data.length > 0 && batch.data[0].bracketedData !== void 0;
|
|
4502
|
+
}
|
|
4503
|
+
// ============================================================
|
|
4504
|
+
// Bracketed batch (dbbracketed/ channel)
|
|
4505
|
+
// ============================================================
|
|
4506
|
+
async handleBracketedBatch(collection, payload) {
|
|
4507
|
+
const updatedIds = [];
|
|
4508
|
+
for (const item of payload.data) {
|
|
4509
|
+
const processed = await this.processBracketedItem(collection, item);
|
|
4510
|
+
if (processed) updatedIds.push(String(item._id));
|
|
4511
|
+
}
|
|
4512
|
+
return updatedIds;
|
|
4513
|
+
}
|
|
4514
|
+
async processBracketedItem(collection, item) {
|
|
4515
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
4516
|
+
const localItem = await this.dexieDb.getById(collection, item._id);
|
|
4517
|
+
if (!localItem) {
|
|
4518
|
+
if (item.operation === "insert" && item.bracketedData) {
|
|
4519
|
+
await this.handleServerItemInsert(collection, item.bracketedData);
|
|
4520
|
+
return true;
|
|
4521
|
+
}
|
|
4522
|
+
const fullItem = await this.safeFindById(collection, item._id);
|
|
4523
|
+
if (fullItem) {
|
|
4524
|
+
await this.handleServerItemInsert(collection, fullItem);
|
|
4525
|
+
return true;
|
|
4526
|
+
}
|
|
4527
|
+
return false;
|
|
4528
|
+
}
|
|
4529
|
+
const revAction = this.checkRev(
|
|
4530
|
+
item._rev,
|
|
4531
|
+
item._ts,
|
|
4532
|
+
(_b = item._lastUpdaterId) != null ? _b : (_a = item.bracketedData) == null ? void 0 : _a._lastUpdaterId,
|
|
4533
|
+
localItem
|
|
4534
|
+
);
|
|
4535
|
+
switch (revAction) {
|
|
4536
|
+
case "ignore":
|
|
4537
|
+
return false;
|
|
4538
|
+
case "bump-meta": {
|
|
4539
|
+
await this.dexieDb.save(collection, item._id, {
|
|
4540
|
+
_rev: item._rev,
|
|
4541
|
+
_ts: item._ts
|
|
4542
|
+
});
|
|
4543
|
+
await this.dexieDb.clearDirtyChange(collection, item._id);
|
|
4544
|
+
return false;
|
|
4545
|
+
}
|
|
4546
|
+
case "fetch-full": {
|
|
4547
|
+
const fullItem = await this.safeFindById(collection, item._id);
|
|
4548
|
+
if (fullItem) {
|
|
4549
|
+
await this.dexieDb.insert(collection, fullItem);
|
|
4550
|
+
this.deps.writeToInMemBatch(
|
|
4551
|
+
collection,
|
|
4552
|
+
[this.stripLocalFields(fullItem)],
|
|
4553
|
+
"upsert",
|
|
4554
|
+
{ source: "incremental" }
|
|
4555
|
+
);
|
|
4556
|
+
return true;
|
|
4557
|
+
}
|
|
4558
|
+
this.callOnEbusUpdateSkipped({
|
|
4559
|
+
collection,
|
|
4560
|
+
_id: item._id,
|
|
4561
|
+
incomingRev: (_c = item._rev) != null ? _c : 0,
|
|
4562
|
+
localRev: (_d = localItem._rev) != null ? _d : 0,
|
|
4563
|
+
reason: "fetch-failed"
|
|
4564
|
+
});
|
|
4565
|
+
return false;
|
|
4566
|
+
}
|
|
4567
|
+
case "apply":
|
|
4568
|
+
break;
|
|
4569
|
+
}
|
|
4570
|
+
const pendingChange = this.deps.getPendingChange(collection, item._id);
|
|
4571
|
+
if (pendingChange && item.operation === "update") {
|
|
4572
|
+
const diff = this.stripMetaFromDiff(item.bracketedData);
|
|
4573
|
+
const currentInMemState = Object.assign({}, localItem, pendingChange.data);
|
|
4574
|
+
const merged = applyObjDiff(currentInMemState, diff, item._id, "bracketed");
|
|
4575
|
+
merged._rev = (_e = item._rev) != null ? _e : localItem._rev;
|
|
4576
|
+
merged._ts = (_f = item._ts) != null ? _f : localItem._ts;
|
|
4577
|
+
if (!merged._deleted && !merged._archived) {
|
|
4578
|
+
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4579
|
+
source: "incremental"
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
return true;
|
|
4583
|
+
}
|
|
4584
|
+
const dirtyChange = await this.dexieDb.getDirtyChange(collection, item._id);
|
|
4585
|
+
if (dirtyChange && item.operation === "update") {
|
|
4586
|
+
const diff = this.stripMetaFromDiff(item.bracketedData);
|
|
4587
|
+
const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
|
|
4588
|
+
merged._rev = (_g = item._rev) != null ? _g : localItem._rev;
|
|
4589
|
+
merged._ts = (_h = item._ts) != null ? _h : localItem._ts;
|
|
4590
|
+
if (!merged._deleted && !merged._archived) {
|
|
4591
|
+
await this.dexieDb.insert(collection, merged);
|
|
4592
|
+
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4593
|
+
source: "incremental"
|
|
4594
|
+
});
|
|
4595
|
+
} else {
|
|
4596
|
+
await this.dexieDb.save(collection, item._id, { _deleted: /* @__PURE__ */ new Date() });
|
|
4597
|
+
this.deps.writeToInMemBatch(collection, [{ _id: item._id }], "delete", {
|
|
4598
|
+
source: "incremental"
|
|
4599
|
+
});
|
|
4600
|
+
}
|
|
4601
|
+
return true;
|
|
4602
|
+
}
|
|
4603
|
+
switch (item.operation) {
|
|
4604
|
+
case "insert": {
|
|
4605
|
+
await this.handleServerItemInsert(collection, item.bracketedData);
|
|
4606
|
+
return true;
|
|
4607
|
+
}
|
|
4608
|
+
case "update": {
|
|
4609
|
+
const merged = this.applyBracketedDiff(localItem, item);
|
|
4610
|
+
if (!merged._deleted && !merged._archived) {
|
|
4611
|
+
await this.dexieDb.insert(collection, merged);
|
|
4612
|
+
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4613
|
+
source: "incremental"
|
|
4614
|
+
});
|
|
4615
|
+
} else {
|
|
4616
|
+
await this.dexieDb.deleteOne(collection, item._id);
|
|
4617
|
+
this.deps.writeToInMemBatch(collection, [{ _id: item._id }], "delete", {
|
|
4618
|
+
source: "incremental"
|
|
4619
|
+
});
|
|
4620
|
+
}
|
|
4621
|
+
return true;
|
|
4622
|
+
}
|
|
4623
|
+
case "delete": {
|
|
4624
|
+
await this.handleServerItemDelete(collection, item._id);
|
|
4625
|
+
return true;
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
return false;
|
|
4629
|
+
}
|
|
4630
|
+
/**
|
|
4631
|
+
* Apply a bracketed diff to a local item using cry-db's applyObjDiff.
|
|
4632
|
+
* Strips meta fields from the diff, then applies _rev/_ts from the item level.
|
|
4633
|
+
*/
|
|
4634
|
+
applyBracketedDiff(localItem, item) {
|
|
4635
|
+
var _a, _b;
|
|
4636
|
+
const diff = this.stripMetaFromDiff(item.bracketedData);
|
|
4637
|
+
const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
|
|
4638
|
+
merged._rev = (_a = item._rev) != null ? _a : localItem._rev;
|
|
4639
|
+
merged._ts = (_b = item._ts) != null ? _b : localItem._ts;
|
|
4640
|
+
return merged;
|
|
4641
|
+
}
|
|
4642
|
+
/**
|
|
4643
|
+
* Strip _id, _rev, _ts, _lastUpdaterId from a diff object before applyObjDiff.
|
|
4644
|
+
*
|
|
4645
|
+
* IMPORTANT: Uses for...in instead of Object.keys() because Object.keys()
|
|
4646
|
+
* skips properties whose value is undefined. Bracketed diffs may contain
|
|
4647
|
+
* {field: undefined} to signal field deletion (translated by applyObjDiff
|
|
4648
|
+
* to deleteByPath). Skipping such keys would silently ignore deletions.
|
|
4649
|
+
*/
|
|
4650
|
+
stripMetaFromDiff(diff) {
|
|
4651
|
+
if (diff == null || typeof diff !== "object") return diff;
|
|
4652
|
+
const result = {};
|
|
4653
|
+
for (const key in diff) {
|
|
4654
|
+
if (!Object.prototype.hasOwnProperty.call(diff, key)) continue;
|
|
4655
|
+
if (key === "_id" || key === "_rev" || key === "_ts" || key === "_lastUpdaterId") continue;
|
|
4656
|
+
result[key] = diff[key];
|
|
4657
|
+
}
|
|
4658
|
+
return result;
|
|
4659
|
+
}
|
|
4660
|
+
// ============================================================
|
|
4661
|
+
// Legacy batch (db/ channel)
|
|
4662
|
+
// ============================================================
|
|
4663
|
+
async handleLegacyBatch(collection, payload) {
|
|
4465
4664
|
const updatedIds = [];
|
|
4466
4665
|
switch (payload.operation) {
|
|
4467
4666
|
case "insert": {
|
|
4468
4667
|
const serverItem = payload.data;
|
|
4469
4668
|
if (serverItem) {
|
|
4470
|
-
await this.handleServerItemInsert(
|
|
4669
|
+
await this.handleServerItemInsert(collection, serverItem);
|
|
4471
4670
|
updatedIds.push(String(serverItem._id));
|
|
4472
4671
|
}
|
|
4473
4672
|
break;
|
|
@@ -4475,20 +4674,14 @@ var ServerUpdateHandler = class {
|
|
|
4475
4674
|
case "update": {
|
|
4476
4675
|
const deltaData = payload.data;
|
|
4477
4676
|
if (deltaData && deltaData._id) {
|
|
4478
|
-
const localItem = await this.dexieDb.getById(
|
|
4479
|
-
collectionName,
|
|
4480
|
-
deltaData._id
|
|
4481
|
-
);
|
|
4677
|
+
const localItem = await this.dexieDb.getById(collection, deltaData._id);
|
|
4482
4678
|
if (localItem) {
|
|
4483
|
-
await this.handleServerItemUpdate(
|
|
4679
|
+
await this.handleServerItemUpdate(collection, localItem, deltaData);
|
|
4484
4680
|
updatedIds.push(String(deltaData._id));
|
|
4485
4681
|
} else {
|
|
4486
|
-
const fullItem = await this.
|
|
4487
|
-
collectionName,
|
|
4488
|
-
deltaData._id
|
|
4489
|
-
);
|
|
4682
|
+
const fullItem = await this.safeFindById(collection, deltaData._id);
|
|
4490
4683
|
if (fullItem) {
|
|
4491
|
-
await this.handleServerItemInsert(
|
|
4684
|
+
await this.handleServerItemInsert(collection, fullItem);
|
|
4492
4685
|
updatedIds.push(String(fullItem._id));
|
|
4493
4686
|
}
|
|
4494
4687
|
}
|
|
@@ -4497,7 +4690,7 @@ var ServerUpdateHandler = class {
|
|
|
4497
4690
|
}
|
|
4498
4691
|
case "delete": {
|
|
4499
4692
|
const deleteData = payload.data;
|
|
4500
|
-
await this.handleServerItemDelete(
|
|
4693
|
+
await this.handleServerItemDelete(collection, deleteData._id);
|
|
4501
4694
|
updatedIds.push(String(deleteData._id));
|
|
4502
4695
|
break;
|
|
4503
4696
|
}
|
|
@@ -4516,9 +4709,7 @@ var ServerUpdateHandler = class {
|
|
|
4516
4709
|
}
|
|
4517
4710
|
if (inserts.length > 0) {
|
|
4518
4711
|
await Promise.all(
|
|
4519
|
-
inserts.map(
|
|
4520
|
-
(serverItem) => this.handleServerItemInsert(collectionName, serverItem)
|
|
4521
|
-
)
|
|
4712
|
+
inserts.map((serverItem) => this.handleServerItemInsert(collection, serverItem))
|
|
4522
4713
|
);
|
|
4523
4714
|
for (const serverItem of inserts) {
|
|
4524
4715
|
updatedIds.push(String(serverItem._id));
|
|
@@ -4526,7 +4717,7 @@ var ServerUpdateHandler = class {
|
|
|
4526
4717
|
}
|
|
4527
4718
|
if (updates.length > 0) {
|
|
4528
4719
|
const updateIds = updates.map((u) => u._id);
|
|
4529
|
-
const localItems = await this.dexieDb.getByIds(
|
|
4720
|
+
const localItems = await this.dexieDb.getByIds(collection, updateIds);
|
|
4530
4721
|
const missingIds = [];
|
|
4531
4722
|
const updatePromises = [];
|
|
4532
4723
|
for (let i = 0; i < updates.length; i++) {
|
|
@@ -4534,7 +4725,7 @@ var ServerUpdateHandler = class {
|
|
|
4534
4725
|
const localItem = localItems[i];
|
|
4535
4726
|
if (localItem) {
|
|
4536
4727
|
updatePromises.push(
|
|
4537
|
-
this.handleServerItemUpdate(
|
|
4728
|
+
this.handleServerItemUpdate(collection, localItem, deltaData)
|
|
4538
4729
|
);
|
|
4539
4730
|
updatedIds.push(String(deltaData._id));
|
|
4540
4731
|
} else {
|
|
@@ -4544,15 +4735,13 @@ var ServerUpdateHandler = class {
|
|
|
4544
4735
|
if (updatePromises.length > 0) await Promise.all(updatePromises);
|
|
4545
4736
|
if (missingIds.length > 0) {
|
|
4546
4737
|
const fullItems = await this.restInterface.findByIds(
|
|
4547
|
-
|
|
4738
|
+
collection,
|
|
4548
4739
|
missingIds
|
|
4549
4740
|
);
|
|
4550
4741
|
const insertPromises = [];
|
|
4551
4742
|
for (const fullItem of fullItems) {
|
|
4552
4743
|
if (!fullItem) continue;
|
|
4553
|
-
insertPromises.push(
|
|
4554
|
-
this.handleServerItemInsert(collectionName, fullItem)
|
|
4555
|
-
);
|
|
4744
|
+
insertPromises.push(this.handleServerItemInsert(collection, fullItem));
|
|
4556
4745
|
updatedIds.push(String(fullItem._id));
|
|
4557
4746
|
}
|
|
4558
4747
|
if (insertPromises.length > 0) await Promise.all(insertPromises);
|
|
@@ -4560,7 +4749,7 @@ var ServerUpdateHandler = class {
|
|
|
4560
4749
|
}
|
|
4561
4750
|
if (deletes.length > 0) {
|
|
4562
4751
|
await Promise.all(
|
|
4563
|
-
deletes.map((d) => this.handleServerItemDelete(
|
|
4752
|
+
deletes.map((d) => this.handleServerItemDelete(collection, d._id))
|
|
4564
4753
|
);
|
|
4565
4754
|
for (const deleteData of deletes) {
|
|
4566
4755
|
updatedIds.push(String(deleteData._id));
|
|
@@ -4569,10 +4758,53 @@ var ServerUpdateHandler = class {
|
|
|
4569
4758
|
break;
|
|
4570
4759
|
}
|
|
4571
4760
|
}
|
|
4572
|
-
|
|
4573
|
-
|
|
4761
|
+
return updatedIds;
|
|
4762
|
+
}
|
|
4763
|
+
// ============================================================
|
|
4764
|
+
// Rev-check (shared across both channels)
|
|
4765
|
+
// ============================================================
|
|
4766
|
+
/**
|
|
4767
|
+
* Determine action based on incoming vs local revision.
|
|
4768
|
+
*
|
|
4769
|
+
* | incomingRev | Action | Reason |
|
|
4770
|
+
* |-------------|-------------|-------------------------------------------|
|
|
4771
|
+
* | ≤ localRev | ignore | Stale / duplicate / self-echo after WB |
|
|
4772
|
+
* | = local+1 | bump-meta | Self-echo before WB (only if updaterId matches) |
|
|
4773
|
+
* | = local+1 | apply | External update (updaterId mismatch) |
|
|
4774
|
+
* | > local+1 | fetch-full | Gap — missed intermediate updates; fetch |
|
|
4775
|
+
* | | full doc since we're online (WS connected).|
|
|
4776
|
+
* | undefined | apply | Legacy server / no rev tracking |
|
|
4777
|
+
*/
|
|
4778
|
+
checkRev(incomingRev, incomingTs, incomingUpdaterId, localItem) {
|
|
4779
|
+
const localRev = localItem._rev;
|
|
4780
|
+
if (typeof incomingRev !== "number" || typeof localRev !== "number") {
|
|
4781
|
+
return "apply";
|
|
4782
|
+
}
|
|
4783
|
+
if (incomingRev <= localRev) {
|
|
4784
|
+
return "ignore";
|
|
4785
|
+
}
|
|
4786
|
+
if (incomingRev === localRev + 1) {
|
|
4787
|
+
const updaterId = incomingUpdaterId != null ? incomingUpdaterId : localItem._lastUpdaterId;
|
|
4788
|
+
if (updaterId === this.updaterId) {
|
|
4789
|
+
return "bump-meta";
|
|
4790
|
+
}
|
|
4791
|
+
return "apply";
|
|
4792
|
+
}
|
|
4793
|
+
return "fetch-full";
|
|
4794
|
+
}
|
|
4795
|
+
// ============================================================
|
|
4796
|
+
// Safe fetch helper
|
|
4797
|
+
// ============================================================
|
|
4798
|
+
async safeFindById(collection, id) {
|
|
4799
|
+
try {
|
|
4800
|
+
return await this.restInterface.findById(collection, id);
|
|
4801
|
+
} catch (e) {
|
|
4802
|
+
return null;
|
|
4574
4803
|
}
|
|
4575
4804
|
}
|
|
4805
|
+
// ============================================================
|
|
4806
|
+
// Per-item handlers (unchanged logic, now called from both branches)
|
|
4807
|
+
// ============================================================
|
|
4576
4808
|
/**
|
|
4577
4809
|
* Handle server item insert.
|
|
4578
4810
|
*/
|
|
@@ -4596,17 +4828,27 @@ var ServerUpdateHandler = class {
|
|
|
4596
4828
|
await this.dexieDb.clearDirtyChange(collection, serverItem._id);
|
|
4597
4829
|
}
|
|
4598
4830
|
if (!serverItem._deleted && !serverItem._archived) {
|
|
4599
|
-
this.deps.writeToInMemBatch(
|
|
4831
|
+
this.deps.writeToInMemBatch(
|
|
4832
|
+
collection,
|
|
4833
|
+
[this.stripLocalFields(serverItem)],
|
|
4834
|
+
"upsert",
|
|
4835
|
+
{ source: "incremental" }
|
|
4836
|
+
);
|
|
4600
4837
|
}
|
|
4601
4838
|
} else {
|
|
4602
4839
|
await this.dexieDb.insert(collection, serverItem);
|
|
4603
4840
|
if (!serverItem._deleted && !serverItem._archived) {
|
|
4604
|
-
this.deps.writeToInMemBatch(
|
|
4841
|
+
this.deps.writeToInMemBatch(
|
|
4842
|
+
collection,
|
|
4843
|
+
[this.stripLocalFields(serverItem)],
|
|
4844
|
+
"upsert",
|
|
4845
|
+
{ source: "incremental" }
|
|
4846
|
+
);
|
|
4605
4847
|
}
|
|
4606
4848
|
}
|
|
4607
4849
|
}
|
|
4608
4850
|
/**
|
|
4609
|
-
* Handle server item update (delta).
|
|
4851
|
+
* Handle server item update (delta) — db/ channel only.
|
|
4610
4852
|
*/
|
|
4611
4853
|
async handleServerItemUpdate(collection, localItem, serverDelta) {
|
|
4612
4854
|
const serverRev = serverDelta._rev;
|
|
@@ -4631,7 +4873,12 @@ var ServerUpdateHandler = class {
|
|
|
4631
4873
|
const currentInMemState = Object.assign({}, localItem, pendingChange.data);
|
|
4632
4874
|
const merged = this.mergeLocalWithDelta(currentInMemState, serverDelta);
|
|
4633
4875
|
if (!merged._deleted && !merged._archived) {
|
|
4634
|
-
this.deps.writeToInMemBatch(
|
|
4876
|
+
this.deps.writeToInMemBatch(
|
|
4877
|
+
collection,
|
|
4878
|
+
[this.stripLocalFields(merged)],
|
|
4879
|
+
"upsert",
|
|
4880
|
+
{ source: "incremental" }
|
|
4881
|
+
);
|
|
4635
4882
|
}
|
|
4636
4883
|
return;
|
|
4637
4884
|
}
|
|
@@ -4643,16 +4890,31 @@ var ServerUpdateHandler = class {
|
|
|
4643
4890
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
4644
4891
|
}
|
|
4645
4892
|
if (!merged._deleted && !merged._archived) {
|
|
4646
|
-
this.deps.writeToInMemBatch(
|
|
4893
|
+
this.deps.writeToInMemBatch(
|
|
4894
|
+
collection,
|
|
4895
|
+
[this.stripLocalFields(merged)],
|
|
4896
|
+
"upsert",
|
|
4897
|
+
{ source: "incremental" }
|
|
4898
|
+
);
|
|
4647
4899
|
}
|
|
4648
4900
|
} else {
|
|
4649
4901
|
if (!metaChanged) return;
|
|
4650
4902
|
const merged = this.mergeLocalWithDelta(localItem, serverDelta);
|
|
4651
4903
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
4652
4904
|
if (!merged._deleted && !merged._archived) {
|
|
4653
|
-
this.deps.writeToInMemBatch(
|
|
4905
|
+
this.deps.writeToInMemBatch(
|
|
4906
|
+
collection,
|
|
4907
|
+
[this.stripLocalFields(merged)],
|
|
4908
|
+
"upsert",
|
|
4909
|
+
{ source: "incremental" }
|
|
4910
|
+
);
|
|
4654
4911
|
} else {
|
|
4655
|
-
this.deps.writeToInMemBatch(
|
|
4912
|
+
this.deps.writeToInMemBatch(
|
|
4913
|
+
collection,
|
|
4914
|
+
[{ _id: serverDelta._id }],
|
|
4915
|
+
"delete",
|
|
4916
|
+
{ source: "incremental" }
|
|
4917
|
+
);
|
|
4656
4918
|
}
|
|
4657
4919
|
}
|
|
4658
4920
|
}
|
|
@@ -4668,7 +4930,12 @@ var ServerUpdateHandler = class {
|
|
|
4668
4930
|
} else {
|
|
4669
4931
|
await this.dexieDb.deleteOne(collection, id);
|
|
4670
4932
|
}
|
|
4671
|
-
this.deps.writeToInMemBatch(
|
|
4933
|
+
this.deps.writeToInMemBatch(
|
|
4934
|
+
collection,
|
|
4935
|
+
[{ _id: id }],
|
|
4936
|
+
"delete",
|
|
4937
|
+
{ source: "incremental" }
|
|
4938
|
+
);
|
|
4672
4939
|
}
|
|
4673
4940
|
// ============================================================
|
|
4674
4941
|
// Private Helpers
|
|
@@ -4717,6 +4984,36 @@ var ServerUpdateHandler = class {
|
|
|
4717
4984
|
}
|
|
4718
4985
|
}
|
|
4719
4986
|
}
|
|
4987
|
+
callOnEbusNotificationReceived(payload) {
|
|
4988
|
+
if (this.callbacks.onEbusNotificationReceived) {
|
|
4989
|
+
try {
|
|
4990
|
+
const channel = this.isBracketedPayload(payload) ? "dbbracketed" : "db";
|
|
4991
|
+
this.callbacks.onEbusNotificationReceived({
|
|
4992
|
+
payload,
|
|
4993
|
+
channel,
|
|
4994
|
+
collection: payload.collection,
|
|
4995
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
4996
|
+
});
|
|
4997
|
+
} catch (err) {
|
|
4998
|
+
console.error(
|
|
4999
|
+
`[ServerUpdateHandler] onEbusNotificationReceived callback failed: ${err}`,
|
|
5000
|
+
err
|
|
5001
|
+
);
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
callOnEbusUpdateSkipped(info) {
|
|
5006
|
+
if (this.callbacks.onEbusUpdateSkipped) {
|
|
5007
|
+
try {
|
|
5008
|
+
const fullInfo = __spreadProps(__spreadValues({}, info), {
|
|
5009
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
5010
|
+
});
|
|
5011
|
+
this.callbacks.onEbusUpdateSkipped(fullInfo);
|
|
5012
|
+
} catch (err) {
|
|
5013
|
+
console.error(`[ServerUpdateHandler] onEbusUpdateSkipped callback failed: ${err}`, err);
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
4720
5017
|
};
|
|
4721
5018
|
|
|
4722
5019
|
// src/db/managers/WakeSyncManager.ts
|
|
@@ -5125,7 +5422,9 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5125
5422
|
dexieDb: this.dexieDb,
|
|
5126
5423
|
restInterface: this.restInterface,
|
|
5127
5424
|
callbacks: {
|
|
5128
|
-
onWsNotification: config.onWsNotification
|
|
5425
|
+
onWsNotification: config.onWsNotification,
|
|
5426
|
+
onEbusNotificationReceived: config.onEbusNotificationReceived,
|
|
5427
|
+
onEbusUpdateSkipped: config.onEbusUpdateSkipped
|
|
5129
5428
|
},
|
|
5130
5429
|
deps: {
|
|
5131
5430
|
isLeader: () => this.leaderElection.isLeader(),
|
|
@@ -11289,6 +11588,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
11289
11588
|
this.reconnectAttempt = 0;
|
|
11290
11589
|
this.currentReconnectDelay = this.reconnectDelayMs;
|
|
11291
11590
|
this.sendSubscribe(`db/${this.dbName}`);
|
|
11591
|
+
this.sendSubscribe(`dbbracketed/${this.dbName}`);
|
|
11292
11592
|
if (this.subscribeServices) {
|
|
11293
11593
|
this.sendSubscribe("ebus/services");
|
|
11294
11594
|
}
|
|
@@ -11431,8 +11731,12 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
11431
11731
|
}
|
|
11432
11732
|
return;
|
|
11433
11733
|
}
|
|
11434
|
-
|
|
11435
|
-
if (
|
|
11734
|
+
let prefix;
|
|
11735
|
+
if (message.channel.startsWith(`db/${this.dbName}/`)) {
|
|
11736
|
+
prefix = `db/${this.dbName}/`;
|
|
11737
|
+
} else if (message.channel.startsWith(`dbbracketed/${this.dbName}/`)) {
|
|
11738
|
+
prefix = `dbbracketed/${this.dbName}/`;
|
|
11739
|
+
} else {
|
|
11436
11740
|
return;
|
|
11437
11741
|
}
|
|
11438
11742
|
const collection = message.channel.slice(prefix.length);
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles:
|
|
5
5
|
* - Routing updates by operation type
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
6
|
+
* - pub_ts dedup across db/ and dbbracketed/ channels
|
|
7
|
+
* - _rev-based stale/self-echo/gap detection
|
|
8
|
+
* - Delta merging with local data (mergeLocalWithDelta for db/, applyObjDiff for dbbracketed/)
|
|
8
9
|
* - Conflict management with pending changes
|
|
9
10
|
* - Broadcasting updates to other tabs
|
|
10
11
|
*/
|
|
11
12
|
import type { Id, LocalDbEntity } from "../../types/DbEntity";
|
|
12
|
-
import type { PublishDataPayload } from "../../types/PublishRevsPayload";
|
|
13
|
+
import type { PublishDataPayload, PublishBracketedPayloadBatch } from "../../types/PublishRevsPayload";
|
|
13
14
|
import type { I_ServerUpdateHandler, ServerUpdateHandlerConfig } from "../types/managers";
|
|
14
15
|
export declare class ServerUpdateHandler implements I_ServerUpdateHandler {
|
|
15
16
|
private readonly tenant;
|
|
@@ -19,17 +20,57 @@ export declare class ServerUpdateHandler implements I_ServerUpdateHandler {
|
|
|
19
20
|
private readonly restInterface;
|
|
20
21
|
private readonly callbacks;
|
|
21
22
|
private readonly deps;
|
|
23
|
+
/** Per-collection last processed pub_ts for cross-channel dedup. */
|
|
24
|
+
private lastProcessedPubTs;
|
|
22
25
|
constructor(config: ServerUpdateHandlerConfig);
|
|
23
26
|
/**
|
|
24
27
|
* Handle incoming server update (WebSocket notification).
|
|
28
|
+
* Accepts both db/ (full-object) and dbbracketed/ (diff) payloads.
|
|
25
29
|
*/
|
|
26
|
-
handleServerUpdate(payload: PublishDataPayload): Promise<void>;
|
|
30
|
+
handleServerUpdate(payload: PublishDataPayload | PublishBracketedPayloadBatch): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Check if this batch was already processed (duplicate or stale pub_ts).
|
|
33
|
+
* Updates lastProcessedPubTs when a new pub_ts is seen.
|
|
34
|
+
*/
|
|
35
|
+
private wasAlreadyProcessed;
|
|
36
|
+
private isBracketedPayload;
|
|
37
|
+
private handleBracketedBatch;
|
|
38
|
+
private processBracketedItem;
|
|
39
|
+
/**
|
|
40
|
+
* Apply a bracketed diff to a local item using cry-db's applyObjDiff.
|
|
41
|
+
* Strips meta fields from the diff, then applies _rev/_ts from the item level.
|
|
42
|
+
*/
|
|
43
|
+
private applyBracketedDiff;
|
|
44
|
+
/**
|
|
45
|
+
* Strip _id, _rev, _ts, _lastUpdaterId from a diff object before applyObjDiff.
|
|
46
|
+
*
|
|
47
|
+
* IMPORTANT: Uses for...in instead of Object.keys() because Object.keys()
|
|
48
|
+
* skips properties whose value is undefined. Bracketed diffs may contain
|
|
49
|
+
* {field: undefined} to signal field deletion (translated by applyObjDiff
|
|
50
|
+
* to deleteByPath). Skipping such keys would silently ignore deletions.
|
|
51
|
+
*/
|
|
52
|
+
private stripMetaFromDiff;
|
|
53
|
+
private handleLegacyBatch;
|
|
54
|
+
/**
|
|
55
|
+
* Determine action based on incoming vs local revision.
|
|
56
|
+
*
|
|
57
|
+
* | incomingRev | Action | Reason |
|
|
58
|
+
* |-------------|-------------|-------------------------------------------|
|
|
59
|
+
* | ≤ localRev | ignore | Stale / duplicate / self-echo after WB |
|
|
60
|
+
* | = local+1 | bump-meta | Self-echo before WB (only if updaterId matches) |
|
|
61
|
+
* | = local+1 | apply | External update (updaterId mismatch) |
|
|
62
|
+
* | > local+1 | fetch-full | Gap — missed intermediate updates; fetch |
|
|
63
|
+
* | | full doc since we're online (WS connected).|
|
|
64
|
+
* | undefined | apply | Legacy server / no rev tracking |
|
|
65
|
+
*/
|
|
66
|
+
private checkRev;
|
|
67
|
+
private safeFindById;
|
|
27
68
|
/**
|
|
28
69
|
* Handle server item insert.
|
|
29
70
|
*/
|
|
30
71
|
handleServerItemInsert(collection: string, serverItem: LocalDbEntity): Promise<void>;
|
|
31
72
|
/**
|
|
32
|
-
* Handle server item update (delta).
|
|
73
|
+
* Handle server item update (delta) — db/ channel only.
|
|
33
74
|
*/
|
|
34
75
|
handleServerItemUpdate(collection: string, localItem: LocalDbEntity, serverDelta: LocalDbEntity): Promise<void>;
|
|
35
76
|
/**
|
|
@@ -42,4 +83,6 @@ export declare class ServerUpdateHandler implements I_ServerUpdateHandler {
|
|
|
42
83
|
private mergeLocalWithDelta;
|
|
43
84
|
private getNewFieldsFromServer;
|
|
44
85
|
private callOnWsNotification;
|
|
86
|
+
private callOnEbusNotificationReceived;
|
|
87
|
+
private callOnEbusUpdateSkipped;
|
|
45
88
|
}
|
|
@@ -7,7 +7,7 @@ import type { I_DexieDb, SyncMeta, MetaUpdateBroadcast } from "../../types/I_Dex
|
|
|
7
7
|
import type { I_InMemDb, SyncSource } from "../../types/I_InMemDb";
|
|
8
8
|
import type { I_RestInterface } from "../../types/I_RestInterface";
|
|
9
9
|
import type { PublishDataPayload } from "../../types/PublishRevsPayload";
|
|
10
|
-
import type { CollectionConfig, SyncInfo, ConflictResolutionReport, ServerWriteRequestInfo, ServerWriteResultInfo, ServerSyncWriteInfo, FindNewerManyCallInfo, FindNewerManyResultInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, CrossTabSyncInfo } from "../../types/I_SyncedDb";
|
|
10
|
+
import type { CollectionConfig, SyncInfo, ConflictResolutionReport, ServerWriteRequestInfo, ServerWriteResultInfo, ServerSyncWriteInfo, FindNewerManyCallInfo, FindNewerManyResultInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, EbusNotificationReceivedInfo, EbusUpdateSkippedInfo, CrossTabSyncInfo } from "../../types/I_SyncedDb";
|
|
11
11
|
import type { PendingChange, UploadResult } from "./internal";
|
|
12
12
|
export interface LeaderElectionCallbacks {
|
|
13
13
|
onBecameLeader?: () => void;
|
|
@@ -351,6 +351,10 @@ export interface I_SyncEngine {
|
|
|
351
351
|
}
|
|
352
352
|
export interface ServerUpdateHandlerCallbacks {
|
|
353
353
|
onWsNotification?: (info: WsNotificationInfo) => void;
|
|
354
|
+
/** Fired for every incoming ebus notification BEFORE any processing. */
|
|
355
|
+
onEbusNotificationReceived?: (info: EbusNotificationReceivedInfo) => void;
|
|
356
|
+
/** Fired when a gap notification is skipped because the fetch failed. */
|
|
357
|
+
onEbusUpdateSkipped?: (info: EbusUpdateSkippedInfo) => void;
|
|
354
358
|
}
|
|
355
359
|
export interface ServerUpdateHandlerDeps {
|
|
356
360
|
isLeader: () => boolean;
|
|
@@ -373,8 +377,8 @@ export interface ServerUpdateHandlerConfig {
|
|
|
373
377
|
deps: ServerUpdateHandlerDeps;
|
|
374
378
|
}
|
|
375
379
|
export interface I_ServerUpdateHandler {
|
|
376
|
-
/** Handle incoming server update. */
|
|
377
|
-
handleServerUpdate(payload: PublishDataPayload): Promise<void>;
|
|
380
|
+
/** Handle incoming server update (db/ or dbbracketed/ channel). */
|
|
381
|
+
handleServerUpdate(payload: PublishDataPayload | import("../../types/PublishRevsPayload").PublishBracketedPayloadBatch): Promise<void>;
|
|
378
382
|
}
|
|
379
383
|
/** Type of event that triggered wake sync */
|
|
380
384
|
export type WakeSyncTrigger = "pageshow" | "focus" | "visibilitychange";
|
|
@@ -341,6 +341,32 @@ export interface WsNotificationInfo {
|
|
|
341
341
|
/** Timestamp when notification was received */
|
|
342
342
|
timestamp: Date;
|
|
343
343
|
}
|
|
344
|
+
/** Info passed to onEbusNotificationReceived — fires for every incoming ebus notification before processing. */
|
|
345
|
+
export interface EbusNotificationReceivedInfo {
|
|
346
|
+
/** The raw notification payload exactly as received from ebus-proxy */
|
|
347
|
+
payload: import("./PublishRevsPayload").PublishDataPayload | import("./PublishRevsPayload").PublishBracketedPayloadBatch;
|
|
348
|
+
/** Which channel the notification arrived on */
|
|
349
|
+
channel: "db" | "dbbracketed";
|
|
350
|
+
/** Collection name */
|
|
351
|
+
collection: string;
|
|
352
|
+
/** Timestamp when notification was received */
|
|
353
|
+
timestamp: Date;
|
|
354
|
+
}
|
|
355
|
+
/** Info passed to onEbusUpdateSkipped — fires when a notification is skipped because the gap fetch failed. */
|
|
356
|
+
export interface EbusUpdateSkippedInfo {
|
|
357
|
+
/** Collection name */
|
|
358
|
+
collection: string;
|
|
359
|
+
/** Document _id */
|
|
360
|
+
_id: Id;
|
|
361
|
+
/** Incoming _rev that caused the gap detection */
|
|
362
|
+
incomingRev: number;
|
|
363
|
+
/** Local _rev at the time of skip */
|
|
364
|
+
localRev: number;
|
|
365
|
+
/** Reason for skipping */
|
|
366
|
+
reason: "fetch-failed";
|
|
367
|
+
/** Timestamp when skip occurred */
|
|
368
|
+
timestamp: Date;
|
|
369
|
+
}
|
|
344
370
|
/**
|
|
345
371
|
* Source of the conflict - where the conflict was detected
|
|
346
372
|
*/
|
|
@@ -756,6 +782,19 @@ export interface SyncedDbConfig {
|
|
|
756
782
|
onWsReconnect?: (attempt: number) => void;
|
|
757
783
|
/** Callback when a WebSocket notification is received */
|
|
758
784
|
onWsNotification?: (info: WsNotificationInfo) => void;
|
|
785
|
+
/**
|
|
786
|
+
* Diagnostic callback fired for EVERY incoming ebus notification BEFORE
|
|
787
|
+
* any processing (dedup, rev-check, apply). Useful for telemetry/debugging.
|
|
788
|
+
* Receives the raw payload + channel info.
|
|
789
|
+
*/
|
|
790
|
+
onEbusNotificationReceived?: (info: EbusNotificationReceivedInfo) => void;
|
|
791
|
+
/**
|
|
792
|
+
* Callback fired when a server update is skipped because the local document
|
|
793
|
+
* is too far behind (gap: incoming._rev > local._rev + 1) and the fetch
|
|
794
|
+
* for the full document failed (network error, timeout, offline).
|
|
795
|
+
* The document stays unchanged; next sync will catch up.
|
|
796
|
+
*/
|
|
797
|
+
onEbusUpdateSkipped?: (info: EbusUpdateSkippedInfo) => void;
|
|
759
798
|
/** Callback when online status changes (after ping success/failure in tryGoOnline) */
|
|
760
799
|
onOnlineStatusChange?: (online: boolean) => void;
|
|
761
800
|
/** Debounce interval for cross-tab sync broadcasts in ms (default: 100) */
|
|
@@ -82,6 +82,7 @@ export type PublishDataPayloadBatchItem = {
|
|
|
82
82
|
export type PublishDataPayloadBatch = PublishDataPayloadBase & {
|
|
83
83
|
operation: 'batch';
|
|
84
84
|
data: PublishDataPayloadBatchItem[];
|
|
85
|
+
pub_ts?: number;
|
|
85
86
|
};
|
|
86
87
|
/**
|
|
87
88
|
* Notifikacija serverja s podatki o spremembah
|
|
@@ -93,5 +94,25 @@ export type PublishDataSpec = {
|
|
|
93
94
|
payload: PublishDataPayload;
|
|
94
95
|
user?: string;
|
|
95
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Payload for bracketed diff notifications (dbbracketed/ channel).
|
|
99
|
+
* Contains the client diff (bracketedData) instead of full parent arrays.
|
|
100
|
+
* _rev and _ts are at item level, outside bracketedData.
|
|
101
|
+
*/
|
|
102
|
+
export type PublishBracketedPayloadBatchItem<T = any> = {
|
|
103
|
+
operation: 'insert' | 'update' | 'delete';
|
|
104
|
+
_id: Id;
|
|
105
|
+
_ts: Timestamp | {
|
|
106
|
+
t: number;
|
|
107
|
+
i: number;
|
|
108
|
+
};
|
|
109
|
+
_rev?: number;
|
|
110
|
+
bracketedData: T;
|
|
111
|
+
};
|
|
112
|
+
export type PublishBracketedPayloadBatch<T = any> = PublishDataPayloadBase & {
|
|
113
|
+
operation: 'batch';
|
|
114
|
+
data: PublishBracketedPayloadBatchItem<T>[];
|
|
115
|
+
pub_ts?: number;
|
|
116
|
+
};
|
|
96
117
|
export type PublishSpec = PublishDataSpec | PublishRevsSpec;
|
|
97
118
|
export {};
|