cry-synced-db-client 0.1.219 → 2.0.0
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,220 @@ 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;
|
|
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
|
+
void 0,
|
|
4533
|
+
// bracketedData doesn't carry _lastUpdaterId
|
|
4534
|
+
localItem
|
|
4535
|
+
);
|
|
4536
|
+
switch (revAction) {
|
|
4537
|
+
case "ignore":
|
|
4538
|
+
return false;
|
|
4539
|
+
case "bump-meta": {
|
|
4540
|
+
await this.dexieDb.save(collection, item._id, {
|
|
4541
|
+
_rev: item._rev,
|
|
4542
|
+
_ts: item._ts
|
|
4543
|
+
});
|
|
4544
|
+
await this.dexieDb.clearDirtyChange(collection, item._id);
|
|
4545
|
+
return false;
|
|
4546
|
+
}
|
|
4547
|
+
case "fetch-full": {
|
|
4548
|
+
const fullItem = await this.safeFindById(collection, item._id);
|
|
4549
|
+
if (fullItem) {
|
|
4550
|
+
await this.dexieDb.insert(collection, fullItem);
|
|
4551
|
+
this.deps.writeToInMemBatch(
|
|
4552
|
+
collection,
|
|
4553
|
+
[this.stripLocalFields(fullItem)],
|
|
4554
|
+
"upsert",
|
|
4555
|
+
{ source: "incremental" }
|
|
4556
|
+
);
|
|
4557
|
+
return true;
|
|
4558
|
+
}
|
|
4559
|
+
this.callOnEbusUpdateSkipped({
|
|
4560
|
+
collection,
|
|
4561
|
+
_id: item._id,
|
|
4562
|
+
incomingRev: (_a = item._rev) != null ? _a : 0,
|
|
4563
|
+
localRev: (_b = localItem._rev) != null ? _b : 0,
|
|
4564
|
+
reason: "fetch-failed"
|
|
4565
|
+
});
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
case "apply":
|
|
4569
|
+
break;
|
|
4570
|
+
}
|
|
4571
|
+
const pendingChange = this.deps.getPendingChange(collection, item._id);
|
|
4572
|
+
if (pendingChange && item.operation === "update") {
|
|
4573
|
+
const diff = this.stripMetaFromDiff(item.bracketedData);
|
|
4574
|
+
const currentInMemState = Object.assign({}, localItem, pendingChange.data);
|
|
4575
|
+
const merged = applyObjDiff(currentInMemState, diff, item._id, "bracketed");
|
|
4576
|
+
merged._rev = (_c = item._rev) != null ? _c : localItem._rev;
|
|
4577
|
+
merged._ts = (_d = item._ts) != null ? _d : localItem._ts;
|
|
4578
|
+
if (!merged._deleted && !merged._archived) {
|
|
4579
|
+
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4580
|
+
source: "incremental"
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
return true;
|
|
4584
|
+
}
|
|
4585
|
+
const dirtyChange = await this.dexieDb.getDirtyChange(collection, item._id);
|
|
4586
|
+
if (dirtyChange && item.operation === "update") {
|
|
4587
|
+
const diff = this.stripMetaFromDiff(item.bracketedData);
|
|
4588
|
+
const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
|
|
4589
|
+
merged._rev = (_e = item._rev) != null ? _e : localItem._rev;
|
|
4590
|
+
merged._ts = (_f = item._ts) != null ? _f : localItem._ts;
|
|
4591
|
+
if (!merged._deleted && !merged._archived) {
|
|
4592
|
+
await this.dexieDb.insert(collection, merged);
|
|
4593
|
+
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4594
|
+
source: "incremental"
|
|
4595
|
+
});
|
|
4596
|
+
} else {
|
|
4597
|
+
await this.dexieDb.save(collection, item._id, { _deleted: /* @__PURE__ */ new Date() });
|
|
4598
|
+
this.deps.writeToInMemBatch(collection, [{ _id: item._id }], "delete", {
|
|
4599
|
+
source: "incremental"
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
return true;
|
|
4603
|
+
}
|
|
4604
|
+
switch (item.operation) {
|
|
4605
|
+
case "insert": {
|
|
4606
|
+
await this.handleServerItemInsert(collection, item.bracketedData);
|
|
4607
|
+
return true;
|
|
4608
|
+
}
|
|
4609
|
+
case "update": {
|
|
4610
|
+
const merged = this.applyBracketedDiff(localItem, item);
|
|
4611
|
+
if (!merged._deleted && !merged._archived) {
|
|
4612
|
+
await this.dexieDb.insert(collection, merged);
|
|
4613
|
+
this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
|
|
4614
|
+
source: "incremental"
|
|
4615
|
+
});
|
|
4616
|
+
} else {
|
|
4617
|
+
await this.dexieDb.deleteOne(collection, item._id);
|
|
4618
|
+
this.deps.writeToInMemBatch(collection, [{ _id: item._id }], "delete", {
|
|
4619
|
+
source: "incremental"
|
|
4620
|
+
});
|
|
4621
|
+
}
|
|
4622
|
+
return true;
|
|
4623
|
+
}
|
|
4624
|
+
case "delete": {
|
|
4625
|
+
await this.handleServerItemDelete(collection, item._id);
|
|
4626
|
+
return true;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return false;
|
|
4630
|
+
}
|
|
4631
|
+
/**
|
|
4632
|
+
* Apply a bracketed diff to a local item using cry-db's applyObjDiff.
|
|
4633
|
+
* Strips meta fields from the diff, then applies _rev/_ts from the item level.
|
|
4634
|
+
*/
|
|
4635
|
+
applyBracketedDiff(localItem, item) {
|
|
4636
|
+
var _a, _b;
|
|
4637
|
+
const diff = this.stripMetaFromDiff(item.bracketedData);
|
|
4638
|
+
const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
|
|
4639
|
+
merged._rev = (_a = item._rev) != null ? _a : localItem._rev;
|
|
4640
|
+
merged._ts = (_b = item._ts) != null ? _b : localItem._ts;
|
|
4641
|
+
return merged;
|
|
4642
|
+
}
|
|
4643
|
+
/**
|
|
4644
|
+
* Strip _id, _rev, _ts, _lastUpdaterId from a diff object before applyObjDiff.
|
|
4645
|
+
*
|
|
4646
|
+
* IMPORTANT: Uses for...in instead of Object.keys() because Object.keys()
|
|
4647
|
+
* skips properties whose value is undefined. Bracketed diffs may contain
|
|
4648
|
+
* {field: undefined} to signal field deletion (translated by applyObjDiff
|
|
4649
|
+
* to deleteByPath). Skipping such keys would silently ignore deletions.
|
|
4650
|
+
*/
|
|
4651
|
+
stripMetaFromDiff(diff) {
|
|
4652
|
+
if (diff == null || typeof diff !== "object") return diff;
|
|
4653
|
+
const result = {};
|
|
4654
|
+
for (const key in diff) {
|
|
4655
|
+
if (!Object.prototype.hasOwnProperty.call(diff, key)) continue;
|
|
4656
|
+
if (key === "_id" || key === "_rev" || key === "_ts" || key === "_lastUpdaterId") continue;
|
|
4657
|
+
result[key] = diff[key];
|
|
4658
|
+
}
|
|
4659
|
+
return result;
|
|
4660
|
+
}
|
|
4661
|
+
// ============================================================
|
|
4662
|
+
// Legacy batch (db/ channel)
|
|
4663
|
+
// ============================================================
|
|
4664
|
+
async handleLegacyBatch(collection, payload) {
|
|
4465
4665
|
const updatedIds = [];
|
|
4466
4666
|
switch (payload.operation) {
|
|
4467
4667
|
case "insert": {
|
|
4468
4668
|
const serverItem = payload.data;
|
|
4469
4669
|
if (serverItem) {
|
|
4470
|
-
await this.handleServerItemInsert(
|
|
4670
|
+
await this.handleServerItemInsert(collection, serverItem);
|
|
4471
4671
|
updatedIds.push(String(serverItem._id));
|
|
4472
4672
|
}
|
|
4473
4673
|
break;
|
|
@@ -4475,20 +4675,14 @@ var ServerUpdateHandler = class {
|
|
|
4475
4675
|
case "update": {
|
|
4476
4676
|
const deltaData = payload.data;
|
|
4477
4677
|
if (deltaData && deltaData._id) {
|
|
4478
|
-
const localItem = await this.dexieDb.getById(
|
|
4479
|
-
collectionName,
|
|
4480
|
-
deltaData._id
|
|
4481
|
-
);
|
|
4678
|
+
const localItem = await this.dexieDb.getById(collection, deltaData._id);
|
|
4482
4679
|
if (localItem) {
|
|
4483
|
-
await this.handleServerItemUpdate(
|
|
4680
|
+
await this.handleServerItemUpdate(collection, localItem, deltaData);
|
|
4484
4681
|
updatedIds.push(String(deltaData._id));
|
|
4485
4682
|
} else {
|
|
4486
|
-
const fullItem = await this.
|
|
4487
|
-
collectionName,
|
|
4488
|
-
deltaData._id
|
|
4489
|
-
);
|
|
4683
|
+
const fullItem = await this.safeFindById(collection, deltaData._id);
|
|
4490
4684
|
if (fullItem) {
|
|
4491
|
-
await this.handleServerItemInsert(
|
|
4685
|
+
await this.handleServerItemInsert(collection, fullItem);
|
|
4492
4686
|
updatedIds.push(String(fullItem._id));
|
|
4493
4687
|
}
|
|
4494
4688
|
}
|
|
@@ -4497,7 +4691,7 @@ var ServerUpdateHandler = class {
|
|
|
4497
4691
|
}
|
|
4498
4692
|
case "delete": {
|
|
4499
4693
|
const deleteData = payload.data;
|
|
4500
|
-
await this.handleServerItemDelete(
|
|
4694
|
+
await this.handleServerItemDelete(collection, deleteData._id);
|
|
4501
4695
|
updatedIds.push(String(deleteData._id));
|
|
4502
4696
|
break;
|
|
4503
4697
|
}
|
|
@@ -4516,9 +4710,7 @@ var ServerUpdateHandler = class {
|
|
|
4516
4710
|
}
|
|
4517
4711
|
if (inserts.length > 0) {
|
|
4518
4712
|
await Promise.all(
|
|
4519
|
-
inserts.map(
|
|
4520
|
-
(serverItem) => this.handleServerItemInsert(collectionName, serverItem)
|
|
4521
|
-
)
|
|
4713
|
+
inserts.map((serverItem) => this.handleServerItemInsert(collection, serverItem))
|
|
4522
4714
|
);
|
|
4523
4715
|
for (const serverItem of inserts) {
|
|
4524
4716
|
updatedIds.push(String(serverItem._id));
|
|
@@ -4526,7 +4718,7 @@ var ServerUpdateHandler = class {
|
|
|
4526
4718
|
}
|
|
4527
4719
|
if (updates.length > 0) {
|
|
4528
4720
|
const updateIds = updates.map((u) => u._id);
|
|
4529
|
-
const localItems = await this.dexieDb.getByIds(
|
|
4721
|
+
const localItems = await this.dexieDb.getByIds(collection, updateIds);
|
|
4530
4722
|
const missingIds = [];
|
|
4531
4723
|
const updatePromises = [];
|
|
4532
4724
|
for (let i = 0; i < updates.length; i++) {
|
|
@@ -4534,7 +4726,7 @@ var ServerUpdateHandler = class {
|
|
|
4534
4726
|
const localItem = localItems[i];
|
|
4535
4727
|
if (localItem) {
|
|
4536
4728
|
updatePromises.push(
|
|
4537
|
-
this.handleServerItemUpdate(
|
|
4729
|
+
this.handleServerItemUpdate(collection, localItem, deltaData)
|
|
4538
4730
|
);
|
|
4539
4731
|
updatedIds.push(String(deltaData._id));
|
|
4540
4732
|
} else {
|
|
@@ -4544,15 +4736,13 @@ var ServerUpdateHandler = class {
|
|
|
4544
4736
|
if (updatePromises.length > 0) await Promise.all(updatePromises);
|
|
4545
4737
|
if (missingIds.length > 0) {
|
|
4546
4738
|
const fullItems = await this.restInterface.findByIds(
|
|
4547
|
-
|
|
4739
|
+
collection,
|
|
4548
4740
|
missingIds
|
|
4549
4741
|
);
|
|
4550
4742
|
const insertPromises = [];
|
|
4551
4743
|
for (const fullItem of fullItems) {
|
|
4552
4744
|
if (!fullItem) continue;
|
|
4553
|
-
insertPromises.push(
|
|
4554
|
-
this.handleServerItemInsert(collectionName, fullItem)
|
|
4555
|
-
);
|
|
4745
|
+
insertPromises.push(this.handleServerItemInsert(collection, fullItem));
|
|
4556
4746
|
updatedIds.push(String(fullItem._id));
|
|
4557
4747
|
}
|
|
4558
4748
|
if (insertPromises.length > 0) await Promise.all(insertPromises);
|
|
@@ -4560,7 +4750,7 @@ var ServerUpdateHandler = class {
|
|
|
4560
4750
|
}
|
|
4561
4751
|
if (deletes.length > 0) {
|
|
4562
4752
|
await Promise.all(
|
|
4563
|
-
deletes.map((d) => this.handleServerItemDelete(
|
|
4753
|
+
deletes.map((d) => this.handleServerItemDelete(collection, d._id))
|
|
4564
4754
|
);
|
|
4565
4755
|
for (const deleteData of deletes) {
|
|
4566
4756
|
updatedIds.push(String(deleteData._id));
|
|
@@ -4569,10 +4759,53 @@ var ServerUpdateHandler = class {
|
|
|
4569
4759
|
break;
|
|
4570
4760
|
}
|
|
4571
4761
|
}
|
|
4572
|
-
|
|
4573
|
-
|
|
4762
|
+
return updatedIds;
|
|
4763
|
+
}
|
|
4764
|
+
// ============================================================
|
|
4765
|
+
// Rev-check (shared across both channels)
|
|
4766
|
+
// ============================================================
|
|
4767
|
+
/**
|
|
4768
|
+
* Determine action based on incoming vs local revision.
|
|
4769
|
+
*
|
|
4770
|
+
* | incomingRev | Action | Reason |
|
|
4771
|
+
* |-------------|-------------|-------------------------------------------|
|
|
4772
|
+
* | ≤ localRev | ignore | Stale / duplicate / self-echo after WB |
|
|
4773
|
+
* | = local+1 | bump-meta | Self-echo before WB (only if updaterId matches) |
|
|
4774
|
+
* | = local+1 | apply | External update (updaterId mismatch) |
|
|
4775
|
+
* | > local+1 | fetch-full | Gap — missed intermediate updates; fetch |
|
|
4776
|
+
* | | full doc since we're online (WS connected).|
|
|
4777
|
+
* | undefined | apply | Legacy server / no rev tracking |
|
|
4778
|
+
*/
|
|
4779
|
+
checkRev(incomingRev, incomingTs, incomingUpdaterId, localItem) {
|
|
4780
|
+
const localRev = localItem._rev;
|
|
4781
|
+
if (typeof incomingRev !== "number" || typeof localRev !== "number") {
|
|
4782
|
+
return "apply";
|
|
4783
|
+
}
|
|
4784
|
+
if (incomingRev <= localRev) {
|
|
4785
|
+
return "ignore";
|
|
4786
|
+
}
|
|
4787
|
+
if (incomingRev === localRev + 1) {
|
|
4788
|
+
const updaterId = incomingUpdaterId != null ? incomingUpdaterId : localItem._lastUpdaterId;
|
|
4789
|
+
if (updaterId === this.updaterId) {
|
|
4790
|
+
return "bump-meta";
|
|
4791
|
+
}
|
|
4792
|
+
return "apply";
|
|
4793
|
+
}
|
|
4794
|
+
return "fetch-full";
|
|
4795
|
+
}
|
|
4796
|
+
// ============================================================
|
|
4797
|
+
// Safe fetch helper
|
|
4798
|
+
// ============================================================
|
|
4799
|
+
async safeFindById(collection, id) {
|
|
4800
|
+
try {
|
|
4801
|
+
return await this.restInterface.findById(collection, id);
|
|
4802
|
+
} catch (e) {
|
|
4803
|
+
return null;
|
|
4574
4804
|
}
|
|
4575
4805
|
}
|
|
4806
|
+
// ============================================================
|
|
4807
|
+
// Per-item handlers (unchanged logic, now called from both branches)
|
|
4808
|
+
// ============================================================
|
|
4576
4809
|
/**
|
|
4577
4810
|
* Handle server item insert.
|
|
4578
4811
|
*/
|
|
@@ -4596,17 +4829,27 @@ var ServerUpdateHandler = class {
|
|
|
4596
4829
|
await this.dexieDb.clearDirtyChange(collection, serverItem._id);
|
|
4597
4830
|
}
|
|
4598
4831
|
if (!serverItem._deleted && !serverItem._archived) {
|
|
4599
|
-
this.deps.writeToInMemBatch(
|
|
4832
|
+
this.deps.writeToInMemBatch(
|
|
4833
|
+
collection,
|
|
4834
|
+
[this.stripLocalFields(serverItem)],
|
|
4835
|
+
"upsert",
|
|
4836
|
+
{ source: "incremental" }
|
|
4837
|
+
);
|
|
4600
4838
|
}
|
|
4601
4839
|
} else {
|
|
4602
4840
|
await this.dexieDb.insert(collection, serverItem);
|
|
4603
4841
|
if (!serverItem._deleted && !serverItem._archived) {
|
|
4604
|
-
this.deps.writeToInMemBatch(
|
|
4842
|
+
this.deps.writeToInMemBatch(
|
|
4843
|
+
collection,
|
|
4844
|
+
[this.stripLocalFields(serverItem)],
|
|
4845
|
+
"upsert",
|
|
4846
|
+
{ source: "incremental" }
|
|
4847
|
+
);
|
|
4605
4848
|
}
|
|
4606
4849
|
}
|
|
4607
4850
|
}
|
|
4608
4851
|
/**
|
|
4609
|
-
* Handle server item update (delta).
|
|
4852
|
+
* Handle server item update (delta) — db/ channel only.
|
|
4610
4853
|
*/
|
|
4611
4854
|
async handleServerItemUpdate(collection, localItem, serverDelta) {
|
|
4612
4855
|
const serverRev = serverDelta._rev;
|
|
@@ -4631,7 +4874,12 @@ var ServerUpdateHandler = class {
|
|
|
4631
4874
|
const currentInMemState = Object.assign({}, localItem, pendingChange.data);
|
|
4632
4875
|
const merged = this.mergeLocalWithDelta(currentInMemState, serverDelta);
|
|
4633
4876
|
if (!merged._deleted && !merged._archived) {
|
|
4634
|
-
this.deps.writeToInMemBatch(
|
|
4877
|
+
this.deps.writeToInMemBatch(
|
|
4878
|
+
collection,
|
|
4879
|
+
[this.stripLocalFields(merged)],
|
|
4880
|
+
"upsert",
|
|
4881
|
+
{ source: "incremental" }
|
|
4882
|
+
);
|
|
4635
4883
|
}
|
|
4636
4884
|
return;
|
|
4637
4885
|
}
|
|
@@ -4643,16 +4891,31 @@ var ServerUpdateHandler = class {
|
|
|
4643
4891
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
4644
4892
|
}
|
|
4645
4893
|
if (!merged._deleted && !merged._archived) {
|
|
4646
|
-
this.deps.writeToInMemBatch(
|
|
4894
|
+
this.deps.writeToInMemBatch(
|
|
4895
|
+
collection,
|
|
4896
|
+
[this.stripLocalFields(merged)],
|
|
4897
|
+
"upsert",
|
|
4898
|
+
{ source: "incremental" }
|
|
4899
|
+
);
|
|
4647
4900
|
}
|
|
4648
4901
|
} else {
|
|
4649
4902
|
if (!metaChanged) return;
|
|
4650
4903
|
const merged = this.mergeLocalWithDelta(localItem, serverDelta);
|
|
4651
4904
|
await this.dexieDb.save(collection, serverDelta._id, merged);
|
|
4652
4905
|
if (!merged._deleted && !merged._archived) {
|
|
4653
|
-
this.deps.writeToInMemBatch(
|
|
4906
|
+
this.deps.writeToInMemBatch(
|
|
4907
|
+
collection,
|
|
4908
|
+
[this.stripLocalFields(merged)],
|
|
4909
|
+
"upsert",
|
|
4910
|
+
{ source: "incremental" }
|
|
4911
|
+
);
|
|
4654
4912
|
} else {
|
|
4655
|
-
this.deps.writeToInMemBatch(
|
|
4913
|
+
this.deps.writeToInMemBatch(
|
|
4914
|
+
collection,
|
|
4915
|
+
[{ _id: serverDelta._id }],
|
|
4916
|
+
"delete",
|
|
4917
|
+
{ source: "incremental" }
|
|
4918
|
+
);
|
|
4656
4919
|
}
|
|
4657
4920
|
}
|
|
4658
4921
|
}
|
|
@@ -4668,7 +4931,12 @@ var ServerUpdateHandler = class {
|
|
|
4668
4931
|
} else {
|
|
4669
4932
|
await this.dexieDb.deleteOne(collection, id);
|
|
4670
4933
|
}
|
|
4671
|
-
this.deps.writeToInMemBatch(
|
|
4934
|
+
this.deps.writeToInMemBatch(
|
|
4935
|
+
collection,
|
|
4936
|
+
[{ _id: id }],
|
|
4937
|
+
"delete",
|
|
4938
|
+
{ source: "incremental" }
|
|
4939
|
+
);
|
|
4672
4940
|
}
|
|
4673
4941
|
// ============================================================
|
|
4674
4942
|
// Private Helpers
|
|
@@ -4717,6 +4985,36 @@ var ServerUpdateHandler = class {
|
|
|
4717
4985
|
}
|
|
4718
4986
|
}
|
|
4719
4987
|
}
|
|
4988
|
+
callOnEbusNotificationReceived(payload) {
|
|
4989
|
+
if (this.callbacks.onEbusNotificationReceived) {
|
|
4990
|
+
try {
|
|
4991
|
+
const channel = this.isBracketedPayload(payload) ? "dbbracketed" : "db";
|
|
4992
|
+
this.callbacks.onEbusNotificationReceived({
|
|
4993
|
+
payload,
|
|
4994
|
+
channel,
|
|
4995
|
+
collection: payload.collection,
|
|
4996
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
4997
|
+
});
|
|
4998
|
+
} catch (err) {
|
|
4999
|
+
console.error(
|
|
5000
|
+
`[ServerUpdateHandler] onEbusNotificationReceived callback failed: ${err}`,
|
|
5001
|
+
err
|
|
5002
|
+
);
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
callOnEbusUpdateSkipped(info) {
|
|
5007
|
+
if (this.callbacks.onEbusUpdateSkipped) {
|
|
5008
|
+
try {
|
|
5009
|
+
const fullInfo = __spreadProps(__spreadValues({}, info), {
|
|
5010
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
5011
|
+
});
|
|
5012
|
+
this.callbacks.onEbusUpdateSkipped(fullInfo);
|
|
5013
|
+
} catch (err) {
|
|
5014
|
+
console.error(`[ServerUpdateHandler] onEbusUpdateSkipped callback failed: ${err}`, err);
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
4720
5018
|
};
|
|
4721
5019
|
|
|
4722
5020
|
// src/db/managers/WakeSyncManager.ts
|
|
@@ -5125,7 +5423,9 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5125
5423
|
dexieDb: this.dexieDb,
|
|
5126
5424
|
restInterface: this.restInterface,
|
|
5127
5425
|
callbacks: {
|
|
5128
|
-
onWsNotification: config.onWsNotification
|
|
5426
|
+
onWsNotification: config.onWsNotification,
|
|
5427
|
+
onEbusNotificationReceived: config.onEbusNotificationReceived,
|
|
5428
|
+
onEbusUpdateSkipped: config.onEbusUpdateSkipped
|
|
5129
5429
|
},
|
|
5130
5430
|
deps: {
|
|
5131
5431
|
isLeader: () => this.leaderElection.isLeader(),
|
|
@@ -11289,6 +11589,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
11289
11589
|
this.reconnectAttempt = 0;
|
|
11290
11590
|
this.currentReconnectDelay = this.reconnectDelayMs;
|
|
11291
11591
|
this.sendSubscribe(`db/${this.dbName}`);
|
|
11592
|
+
this.sendSubscribe(`dbbracketed/${this.dbName}`);
|
|
11292
11593
|
if (this.subscribeServices) {
|
|
11293
11594
|
this.sendSubscribe("ebus/services");
|
|
11294
11595
|
}
|
|
@@ -11431,8 +11732,12 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
11431
11732
|
}
|
|
11432
11733
|
return;
|
|
11433
11734
|
}
|
|
11434
|
-
|
|
11435
|
-
if (
|
|
11735
|
+
let prefix;
|
|
11736
|
+
if (message.channel.startsWith(`db/${this.dbName}/`)) {
|
|
11737
|
+
prefix = `db/${this.dbName}/`;
|
|
11738
|
+
} else if (message.channel.startsWith(`dbbracketed/${this.dbName}/`)) {
|
|
11739
|
+
prefix = `dbbracketed/${this.dbName}/`;
|
|
11740
|
+
} else {
|
|
11436
11741
|
return;
|
|
11437
11742
|
}
|
|
11438
11743
|
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 {};
|