cry-synced-db-client 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -275,7 +275,7 @@ class SyncedDb {
275
275
  onServerWriteRequest;
276
276
  onServerWriteResult;
277
277
  onFindNewerManyCall;
278
- onFindNewerCall;
278
+ onFindNewerManyResult;
279
279
  onDexieWriteRequest;
280
280
  onDexieWriteResult;
281
281
  onLocalstorageWriteResult;
@@ -326,7 +326,7 @@ class SyncedDb {
326
326
  this.onServerWriteRequest = config.onServerWriteRequest;
327
327
  this.onServerWriteResult = config.onServerWriteResult;
328
328
  this.onFindNewerManyCall = config.onFindNewerManyCall;
329
- this.onFindNewerCall = config.onFindNewerCall;
329
+ this.onFindNewerManyResult = config.onFindNewerManyResult;
330
330
  this.onDexieWriteRequest = config.onDexieWriteRequest;
331
331
  this.onDexieWriteResult = config.onDexieWriteResult;
332
332
  this.onLocalstorageWriteResult = config.onLocalstorageWriteResult;
@@ -911,7 +911,40 @@ class SyncedDb {
911
911
  console.error("onFindNewerManyCall callback failed:", err);
912
912
  }
913
913
  }
914
- const allServerData = await this.withSyncTimeout(this.restInterface.findNewerMany(syncSpecs), "findNewerMany");
914
+ const findNewerManyStartTime = Date.now();
915
+ let allServerData;
916
+ try {
917
+ allServerData = await this.withSyncTimeout(this.restInterface.findNewerMany(syncSpecs), "findNewerMany");
918
+ if (this.onFindNewerManyResult) {
919
+ try {
920
+ this.onFindNewerManyResult({
921
+ specs: syncSpecs,
922
+ results: allServerData,
923
+ durationMs: Date.now() - findNewerManyStartTime,
924
+ success: true,
925
+ calledFrom
926
+ });
927
+ } catch (err) {
928
+ console.error("onFindNewerManyResult callback failed:", err);
929
+ }
930
+ }
931
+ } catch (err) {
932
+ if (this.onFindNewerManyResult) {
933
+ try {
934
+ this.onFindNewerManyResult({
935
+ specs: syncSpecs,
936
+ results: {},
937
+ durationMs: Date.now() - findNewerManyStartTime,
938
+ success: false,
939
+ error: err instanceof Error ? err : new Error(String(err)),
940
+ calledFrom
941
+ });
942
+ } catch (callbackErr) {
943
+ console.error("onFindNewerManyResult callback failed:", callbackErr);
944
+ }
945
+ }
946
+ throw err;
947
+ }
915
948
  for (const [collectionName, config] of this.collections) {
916
949
  const serverData = allServerData[collectionName] || [];
917
950
  receivedCount += serverData.length;
@@ -1278,27 +1311,6 @@ class SyncedDb {
1278
1311
  }
1279
1312
  }
1280
1313
  }
1281
- async syncSingleCollection(collectionName) {
1282
- const config = this.collections.get(collectionName);
1283
- if (!config)
1284
- return;
1285
- const meta = await this.dexieDb.getSyncMeta(collectionName);
1286
- const fromTimestamp = meta?.lastSyncTs || 0;
1287
- if (this.onFindNewerCall) {
1288
- try {
1289
- this.onFindNewerCall({
1290
- collection: collectionName,
1291
- fromTimestamp,
1292
- query: config.query,
1293
- timestamp: new Date
1294
- });
1295
- } catch (err) {
1296
- console.error("onFindNewerCall callback failed:", err);
1297
- }
1298
- }
1299
- const serverData = await this.restInterface.findNewer(collectionName, fromTimestamp, config.query, { returnDeleted: true });
1300
- await this.processIncomingServerData(collectionName, config, serverData);
1301
- }
1302
1314
  async processIncomingServerData(collectionName, config, serverData) {
1303
1315
  let maxTs;
1304
1316
  let conflictsResolved = 0;
@@ -1583,7 +1595,7 @@ class SyncedDb {
1583
1595
  case "insert": {
1584
1596
  const serverItem = payload.data;
1585
1597
  if (serverItem) {
1586
- await this.handleServerItemUpdate(collectionName, serverItem);
1598
+ await this.handleServerItemInsert(collectionName, serverItem);
1587
1599
  if (serverItem._ts) {
1588
1600
  await this.broadcastMetaUpdate(collectionName, serverItem._ts);
1589
1601
  }
@@ -1595,15 +1607,14 @@ class SyncedDb {
1595
1607
  if (deltaData && deltaData._id) {
1596
1608
  const localItem = await this.dexieDb.getById(collectionName, deltaData._id);
1597
1609
  if (localItem) {
1598
- await this.handleServerItemUpdate(collectionName, deltaData);
1610
+ await this.handleServerItemUpdate(collectionName, localItem, deltaData);
1599
1611
  if (deltaData._ts) {
1600
1612
  await this.broadcastMetaUpdate(collectionName, deltaData._ts);
1601
1613
  }
1602
1614
  } else {
1603
- const items = await this.restInterface.findNewer(collectionName, 0, { _id: deltaData._id });
1604
- const fullItem = items[0];
1615
+ const fullItem = await this.restInterface.findById(collectionName, deltaData._id);
1605
1616
  if (fullItem) {
1606
- await this.handleServerItemUpdate(collectionName, fullItem);
1617
+ await this.handleServerItemInsert(collectionName, fullItem);
1607
1618
  if (fullItem._ts) {
1608
1619
  await this.broadcastMetaUpdate(collectionName, fullItem._ts);
1609
1620
  }
@@ -1623,19 +1634,18 @@ class SyncedDb {
1623
1634
  if (item.operation === "insert") {
1624
1635
  const serverItem = item.data;
1625
1636
  if (serverItem) {
1626
- await this.handleServerItemUpdate(collectionName, serverItem);
1637
+ await this.handleServerItemInsert(collectionName, serverItem);
1627
1638
  }
1628
1639
  } else if (item.operation === "update") {
1629
1640
  const deltaData = item.data;
1630
1641
  if (deltaData && deltaData._id) {
1631
1642
  const localItem = await this.dexieDb.getById(collectionName, deltaData._id);
1632
1643
  if (localItem) {
1633
- await this.handleServerItemUpdate(collectionName, deltaData);
1644
+ await this.handleServerItemUpdate(collectionName, localItem, deltaData);
1634
1645
  } else {
1635
- const items = await this.restInterface.findNewer(collectionName, 0, { _id: deltaData._id });
1636
- const fullItem = items[0];
1646
+ const fullItem = await this.restInterface.findById(collectionName, deltaData._id);
1637
1647
  if (fullItem) {
1638
- await this.handleServerItemUpdate(collectionName, fullItem);
1648
+ await this.handleServerItemInsert(collectionName, fullItem);
1639
1649
  }
1640
1650
  }
1641
1651
  }
@@ -1651,61 +1661,69 @@ class SyncedDb {
1651
1661
  }
1652
1662
  }
1653
1663
  }
1654
- async handleServerItemUpdate(collectionName, serverItem) {
1664
+ async handleServerItemInsert(collectionName, serverItem) {
1655
1665
  const localItem = await this.dexieDb.getById(collectionName, serverItem._id);
1656
- const pendingKey = this.getPendingKey(collectionName, serverItem._id);
1666
+ if (localItem) {
1667
+ await this.dexieDb.save(collectionName, serverItem._id, {
1668
+ _rev: serverItem._rev,
1669
+ _ts: serverItem._ts,
1670
+ _dirty: false
1671
+ });
1672
+ } else {
1673
+ await this.dexieDb.insert(collectionName, {
1674
+ ...serverItem,
1675
+ _dirty: false
1676
+ });
1677
+ if (!serverItem._deleted) {
1678
+ this.inMemDb.insert(collectionName, this.stripLocalFields(serverItem));
1679
+ }
1680
+ }
1681
+ }
1682
+ async handleServerItemUpdate(collectionName, localItem, serverDelta) {
1683
+ const isLoopback = serverDelta._lastUpdaterId === this.updaterId && serverDelta._rev !== undefined && localItem._rev !== undefined && serverDelta._rev === localItem._rev + 1;
1684
+ if (isLoopback) {
1685
+ await this.dexieDb.save(collectionName, serverDelta._id, {
1686
+ _rev: serverDelta._rev,
1687
+ _ts: serverDelta._ts,
1688
+ _dirty: false
1689
+ });
1690
+ return;
1691
+ }
1692
+ const pendingKey = this.getPendingKey(collectionName, serverDelta._id);
1657
1693
  const pendingChange = this.pendingChanges.get(pendingKey);
1658
- const hasPendingChanges = !!pendingChange;
1659
- if (hasPendingChanges) {
1660
- const currentMemItem = this.inMemDb.getById(collectionName, serverItem._id);
1694
+ if (pendingChange) {
1695
+ const currentMemItem = this.inMemDb.getById(collectionName, serverDelta._id);
1661
1696
  if (currentMemItem) {
1662
- const merged = this.mergeLocalWithDelta(currentMemItem, serverItem);
1697
+ const merged = this.mergeLocalWithDelta(currentMemItem, serverDelta);
1663
1698
  if (!merged._deleted) {
1664
- this.inMemDb.save(collectionName, serverItem._id, this.stripLocalFields(merged));
1699
+ this.inMemDb.save(collectionName, serverDelta._id, this.stripLocalFields(merged));
1665
1700
  }
1666
1701
  pendingChange.data = {
1667
1702
  ...pendingChange.data,
1668
- ...this.getNewFieldsFromServer(currentMemItem, serverItem)
1703
+ ...this.getNewFieldsFromServer(currentMemItem, serverDelta)
1669
1704
  };
1670
1705
  }
1671
1706
  return;
1672
1707
  }
1673
- if (localItem) {
1674
- if (serverItem._lastUpdaterId === this.updaterId && serverItem._rev !== undefined && localItem._rev !== undefined && serverItem._rev === localItem._rev + 1) {
1675
- await this.dexieDb.save(collectionName, serverItem._id, {
1676
- _rev: serverItem._rev,
1677
- _ts: serverItem._ts,
1678
- _dirty: false
1679
- });
1680
- return;
1681
- }
1682
- if (localItem._dirty) {
1683
- const merged = this.mergeLocalWithDelta(localItem, serverItem);
1684
- await this.dexieDb.save(collectionName, serverItem._id, {
1685
- ...merged,
1686
- _dirty: true
1687
- });
1688
- if (!merged._deleted) {
1689
- this.inMemDb.save(collectionName, serverItem._id, this.stripLocalFields(merged));
1690
- }
1691
- } else {
1692
- await this.dexieDb.save(collectionName, serverItem._id, {
1693
- ...serverItem,
1694
- _dirty: false
1695
- });
1696
- if (!serverItem._deleted) {
1697
- this.inMemDb.save(collectionName, serverItem._id, this.stripLocalFields(serverItem));
1698
- } else {
1699
- this.inMemDb.deleteOne(collectionName, serverItem._id);
1700
- }
1708
+ if (localItem._dirty) {
1709
+ const merged = this.mergeLocalWithDelta(localItem, serverDelta);
1710
+ await this.dexieDb.save(collectionName, serverDelta._id, {
1711
+ ...merged,
1712
+ _dirty: true
1713
+ });
1714
+ if (!merged._deleted) {
1715
+ this.inMemDb.save(collectionName, serverDelta._id, this.stripLocalFields(merged));
1701
1716
  }
1702
1717
  } else {
1703
- await this.dexieDb.insert(collectionName, {
1704
- ...serverItem,
1718
+ const merged = this.mergeLocalWithDelta(localItem, serverDelta);
1719
+ await this.dexieDb.save(collectionName, serverDelta._id, {
1720
+ ...merged,
1705
1721
  _dirty: false
1706
1722
  });
1707
- if (!serverItem._deleted) {
1708
- this.inMemDb.insert(collectionName, this.stripLocalFields(serverItem));
1723
+ if (!merged._deleted) {
1724
+ this.inMemDb.save(collectionName, serverDelta._id, this.stripLocalFields(merged));
1725
+ } else {
1726
+ this.inMemDb.deleteOne(collectionName, serverDelta._id);
1709
1727
  }
1710
1728
  }
1711
1729
  }
@@ -4230,6 +4248,14 @@ class RestProxy {
4230
4248
  return false;
4231
4249
  }
4232
4250
  }
4251
+ async findById(collection, id) {
4252
+ return await this.restCall("findById", { collection, id });
4253
+ }
4254
+ async findByIds(collection, ids) {
4255
+ if (ids.length === 0)
4256
+ return [];
4257
+ return await this.restCall("findByIds", { collection, ids });
4258
+ }
4233
4259
  async findNewer(collection, timestamp, query, opts) {
4234
4260
  return await this.restCall("findNewer", {
4235
4261
  collection,
@@ -1,5 +1,6 @@
1
1
  import type { AggregateOptions, Timestamp } from "mongodb";
2
2
  import type { I_RestInterface, QuerySpec, QueryOpts, GetNewerSpec, BatchSpec, CollectionUpdateRequest, CollectionUpdateResult } from "../types/I_RestInterface";
3
+ import type { Id } from "../types/DbEntity";
3
4
  /** Progress callback for tracking upload progress */
4
5
  export type ProgressCallback = (sentBytes: number, totalBytes: number) => void;
5
6
  export interface RestProxyConfig {
@@ -96,6 +97,8 @@ export declare class RestProxy implements I_RestInterface {
96
97
  /** Combine multiple abort signals into one */
97
98
  private combineSignals;
98
99
  ping(): Promise<boolean>;
100
+ findById<T>(collection: string, id: Id): Promise<T | null>;
101
+ findByIds<T>(collection: string, ids: Id[]): Promise<T[]>;
99
102
  findNewer<T>(collection: string, timestamp: Timestamp | number | string | Date, query?: QuerySpec<T>, opts?: QueryOpts): Promise<T[]>;
100
103
  findNewerMany<T>(spec?: GetNewerSpec<T>[]): Promise<Record<string, any[]>>;
101
104
  deleteOne<T>(collection: string, query: QuerySpec<T>): Promise<T>;
@@ -22,7 +22,7 @@ export declare class SyncedDb implements I_SyncedDb {
22
22
  private onServerWriteRequest?;
23
23
  private onServerWriteResult?;
24
24
  private onFindNewerManyCall?;
25
- private onFindNewerCall?;
25
+ private onFindNewerManyResult?;
26
26
  private onDexieWriteRequest?;
27
27
  private onDexieWriteResult?;
28
28
  private onLocalstorageWriteResult?;
@@ -169,11 +169,6 @@ export declare class SyncedDb implements I_SyncedDb {
169
169
  */
170
170
  private uploadDirtyItemsForCollection;
171
171
  private recoverPendingWrites;
172
- /**
173
- * Sinhronizira eno samo kolekcijo - uporabi se pri handleServerUpdate za updateMany/deleteMany
174
- * Samo obdela prihajajoče podatke, ne pošilja dirty objektov nazaj
175
- */
176
- private syncSingleCollection;
177
172
  /**
178
173
  * Obdela prihajajoče podatke s serverja - razreši konflikte in shrani v Dexie/inMem
179
174
  * Ne pošilja dirty objektov na server (to naredi uploadDirtyItems)
@@ -199,6 +194,16 @@ export declare class SyncedDb implements I_SyncedDb {
199
194
  */
200
195
  private handleCrossTabMetaUpdate;
201
196
  private handleServerUpdate;
197
+ /**
198
+ * Handle server insert notification.
199
+ * Insert notifications contain full object data.
200
+ */
201
+ private handleServerItemInsert;
202
+ /**
203
+ * Handle server update notification (delta).
204
+ * Update notifications contain only changed fields.
205
+ * Caller must provide the existing local item.
206
+ */
202
207
  private handleServerItemUpdate;
203
208
  private getNewFieldsFromServer;
204
209
  /**
@@ -0,0 +1 @@
1
+ export {};
@@ -74,6 +74,10 @@ export type CollectionUpdateResult = {
74
74
  export interface I_RestInterface {
75
75
  /** Preveri povezljivost s serverjem - vrne true če je server dosegljiv */
76
76
  ping(): Promise<boolean>;
77
+ /** Fetch a single item by ID, returns null if not found */
78
+ findById<T>(collection: string, id: Id): Promise<T | null>;
79
+ /** Fetch multiple items by IDs */
80
+ findByIds<T>(collection: string, ids: Id[]): Promise<T[]>;
77
81
  findNewer<T>(collection: string, timestamp: Timestamp | number | string | Date, query?: QuerySpec<T>, opts?: QueryOpts): Promise<T[]>;
78
82
  findNewerMany<T>(spec?: GetNewerSpec<T>[]): Promise<Record<string, any[]>>;
79
83
  deleteOne<T>(collection: string, query: QuerySpec<T>): Promise<T>;
@@ -1,4 +1,4 @@
1
- import type { AggregateOptions, Timestamp } from "mongodb";
1
+ import type { AggregateOptions } from "mongodb";
2
2
  import type { Id, DbEntity, LocalDbEntity } from "./DbEntity";
3
3
  import type { QuerySpec, UpdateSpec, InsertSpec, BatchSpec, I_RestInterface, CollectionUpdateRequest, CollectionUpdateResult, GetNewerSpec } from "./I_RestInterface";
4
4
  import type { I_DexieDb } from "./I_DexieDb";
@@ -72,17 +72,21 @@ export interface FindNewerManyCallInfo {
72
72
  calledFrom?: string;
73
73
  }
74
74
  /**
75
- * Callback payload for findNewer calls
75
+ * Callback payload for findNewerMany results
76
76
  */
77
- export interface FindNewerCallInfo {
78
- /** Collection being queried */
79
- collection: string;
80
- /** Timestamp from which to fetch */
81
- fromTimestamp: Timestamp | number | string | Date;
82
- /** Query filter if any */
83
- query?: QuerySpec<any>;
84
- /** Timestamp when request started */
85
- timestamp: Date;
77
+ export interface FindNewerManyResultInfo {
78
+ /** Specs that were requested */
79
+ specs: GetNewerSpec<any>[];
80
+ /** Results per collection (collection name -> items) */
81
+ results: Record<string, any[]>;
82
+ /** Duration in ms */
83
+ durationMs: number;
84
+ /** Whether the request succeeded */
85
+ success: boolean;
86
+ /** Error if failed */
87
+ error?: Error;
88
+ /** Where sync was called from (for debugging) */
89
+ calledFrom?: string;
86
90
  }
87
91
  /**
88
92
  * Callback payload for Dexie write requests (before writing)
@@ -243,8 +247,8 @@ export interface SyncedDbConfig {
243
247
  onServerWriteResult?: (info: ServerWriteResultInfo) => void;
244
248
  /** Callback when findNewerMany is called */
245
249
  onFindNewerManyCall?: (info: FindNewerManyCallInfo) => void;
246
- /** Callback when findNewer is called */
247
- onFindNewerCall?: (info: FindNewerCallInfo) => void;
250
+ /** Callback when findNewerMany completes */
251
+ onFindNewerManyResult?: (info: FindNewerManyResultInfo) => void;
248
252
  /** Callback before writing to Dexie */
249
253
  onDexieWriteRequest?: (info: DexieWriteRequestInfo) => void;
250
254
  /** Callback after writing to Dexie */
@@ -4,5 +4,5 @@ export type { Obj, QuerySpec, Projection, QueryOpts, KeyOf, InsertKeyOf, InsertS
4
4
  export type { I_InMemDb as InMemDb } from "./I_InMemDb";
5
5
  export type { I_DexieDb as DexieDb, SyncMeta } from "./I_DexieDb";
6
6
  export type { I_ServerUpdateNotifier as ServerUpdateNotifier, ServerUpdateCallback, ServerUpdateNotifierCallbacks } from "./I_ServerUpdateNotifier";
7
- export type { I_SyncedDb as SyncedDb, SyncedDbConfig, CollectionConfig, SyncInfo, ServerWriteRequestInfo, ServerWriteResultInfo, FindNewerManyCallInfo, FindNewerCallInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, InfrastructureErrorType, InfrastructureErrorInfo, ConflictSource, ConflictResolutionReport, } from "./I_SyncedDb";
7
+ export type { I_SyncedDb as SyncedDb, SyncedDbConfig, CollectionConfig, SyncInfo, ServerWriteRequestInfo, ServerWriteResultInfo, FindNewerManyCallInfo, FindNewerManyResultInfo, DexieWriteRequestInfo, DexieWriteResultInfo, LocalstorageWriteResultInfo, WsNotificationInfo, InfrastructureErrorType, InfrastructureErrorInfo, ConflictSource, ConflictResolutionReport, } from "./I_SyncedDb";
8
8
  export type { CollectionConfig as CollectionConfigFull } from "./CollectionConfig";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Test constants for faster test execution
3
+ */
4
+ /** Debounce time for tests (ms) - much shorter than production default of 1000ms */
5
+ export declare const TEST_DEBOUNCE_MS = 5;
6
+ /** Helper to get wait time for debounce (1.2x debounce time) */
7
+ export declare const getDebounceWaitMs: (debounceMs?: number) => number;
8
+ /** REST endpoint for real-life proxy tests */
9
+ export declare const REST_ENDPOINT = "http://localhost:3002";
10
+ /** Test tenant name for RestProxy tests */
11
+ export declare const TEST_TENANT = "test";
12
+ /** API key for RestProxy tests */
13
+ export declare const API_KEY = "1234";
14
+ /** WebSocket URL for ebus-proxy tests */
15
+ export declare const EBUS_PROXY_WS_URL = "ws://localhost:3033";
16
+ /** API key for ebus-proxy tests */
17
+ export declare const EBUS_PROXY_API_KEY = "123";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes