cry-synced-db-client 0.1.71 → 0.1.73

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
@@ -2155,29 +2155,49 @@ class SyncEngine {
2155
2155
  }
2156
2156
  this.callOnFindNewerManyCall(syncSpecs, calledFrom);
2157
2157
  const findNewerManyStartTime = Date.now();
2158
- let allServerData;
2158
+ const allUpdatedIds = {};
2159
+ const collectionState = new Map;
2160
+ for (const [name] of configMap) {
2161
+ collectionState.set(name, {
2162
+ maxTs: undefined,
2163
+ conflicts: 0,
2164
+ updatedIds: [],
2165
+ receivedCount: 0
2166
+ });
2167
+ }
2159
2168
  try {
2160
- allServerData = await this.deps.withSyncTimeout(this.restInterface.findNewerMany(syncSpecs), "findNewerMany");
2161
- this.callOnFindNewerManyResult(syncSpecs, allServerData, findNewerManyStartTime, true, calledFrom);
2169
+ await this.deps.withSyncTimeout(this.restInterface.findNewerManyStream(syncSpecs, async (collection, items) => {
2170
+ const config = configMap.get(collection);
2171
+ if (!config)
2172
+ return;
2173
+ const state = collectionState.get(collection);
2174
+ state.receivedCount += items.length;
2175
+ const stats = await this.processIncomingServerData(collection, config, items);
2176
+ state.conflicts += stats.conflictsResolved;
2177
+ if (stats.maxTs) {
2178
+ if (!state.maxTs || this.compareTimestamps(stats.maxTs, state.maxTs) > 0) {
2179
+ state.maxTs = stats.maxTs;
2180
+ }
2181
+ }
2182
+ state.updatedIds.push(...stats.updatedIds);
2183
+ }), "findNewerManyStream");
2184
+ for (const [name, state] of collectionState) {
2185
+ receivedCount += state.receivedCount;
2186
+ conflictsResolved += state.conflicts;
2187
+ collectionStats[name] = {
2188
+ receivedCount: state.receivedCount,
2189
+ sentCount: 0,
2190
+ receivedItems: []
2191
+ };
2192
+ if (state.updatedIds.length > 0) {
2193
+ allUpdatedIds[name] = state.updatedIds;
2194
+ }
2195
+ }
2196
+ this.callOnFindNewerManyResult(syncSpecs, {}, findNewerManyStartTime, true, calledFrom);
2162
2197
  } catch (err) {
2163
2198
  this.callOnFindNewerManyResult(syncSpecs, {}, findNewerManyStartTime, false, calledFrom, err);
2164
2199
  throw err;
2165
2200
  }
2166
- const allUpdatedIds = {};
2167
- for (const [collectionName, config] of configMap) {
2168
- const serverData = allServerData[collectionName] || [];
2169
- receivedCount += serverData.length;
2170
- collectionStats[collectionName] = {
2171
- receivedCount: serverData.length,
2172
- sentCount: 0,
2173
- receivedItems: serverData
2174
- };
2175
- const stats = await this.processIncomingServerData(collectionName, config, serverData);
2176
- conflictsResolved += stats.conflictsResolved;
2177
- if (stats.updatedIds.length > 0) {
2178
- allUpdatedIds[collectionName] = stats.updatedIds;
2179
- }
2180
- }
2181
2201
  if (Object.keys(allUpdatedIds).length > 0) {
2182
2202
  this.deps.broadcastUpdates(allUpdatedIds);
2183
2203
  }
@@ -2420,60 +2440,69 @@ class SyncEngine {
2420
2440
  }
2421
2441
  return { updatedIds: result.updatedIds };
2422
2442
  }
2443
+ static SYNC_BATCH_SIZE = 200;
2423
2444
  async processIncomingServerData(collectionName, config, serverData) {
2424
2445
  if (serverData.length === 0) {
2425
2446
  return { conflictsResolved: 0, maxTs: undefined, updatedIds: [] };
2426
2447
  }
2427
2448
  let maxTs;
2428
2449
  let conflictsResolved = 0;
2429
- const serverIds = serverData.map((item) => item._id);
2430
- const localItems = await this.dexieDb.getByIds(collectionName, serverIds);
2431
- const dirtyChangesMap = await this.dexieDb.getDirtyChangesBatch(collectionName, serverIds);
2432
- const dexieBatch = [];
2433
- const inMemSaveBatch = [];
2434
- const inMemDeleteIds = [];
2435
- for (let i = 0;i < serverData.length; i++) {
2436
- const serverItem = serverData[i];
2437
- const localItem = localItems[i];
2438
- const dirtyChange = dirtyChangesMap.get(String(serverItem._id));
2439
- if (serverItem._ts) {
2440
- if (!maxTs || this.compareTimestamps(serverItem._ts, maxTs) > 0) {
2441
- maxTs = serverItem._ts;
2450
+ const allUpdatedIds = [];
2451
+ const BATCH = SyncEngine.SYNC_BATCH_SIZE;
2452
+ for (let offset = 0;offset < serverData.length; offset += BATCH) {
2453
+ const chunk = serverData.slice(offset, offset + BATCH);
2454
+ const chunkIds = chunk.map((item) => item._id);
2455
+ const localItems = await this.dexieDb.getByIds(collectionName, chunkIds);
2456
+ const dirtyChangesMap = await this.dexieDb.getDirtyChangesBatch(collectionName, chunkIds);
2457
+ const dexieBatch = [];
2458
+ const inMemSaveBatch = [];
2459
+ const inMemDeleteIds = [];
2460
+ for (let i = 0;i < chunk.length; i++) {
2461
+ const serverItem = chunk[i];
2462
+ const localItem = localItems[i];
2463
+ const dirtyChange = dirtyChangesMap.get(String(serverItem._id));
2464
+ if (serverItem._ts) {
2465
+ if (!maxTs || this.compareTimestamps(serverItem._ts, maxTs) > 0) {
2466
+ maxTs = serverItem._ts;
2467
+ }
2442
2468
  }
2443
- }
2444
- if (localItem) {
2445
- if (dirtyChange) {
2446
- conflictsResolved++;
2447
- const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
2448
- dexieBatch.push(resolved);
2449
- if (!resolved._deleted && !resolved._archived) {
2450
- inMemSaveBatch.push(resolved);
2469
+ if (localItem) {
2470
+ if (dirtyChange) {
2471
+ conflictsResolved++;
2472
+ const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
2473
+ dexieBatch.push(resolved);
2474
+ if (!resolved._deleted && !resolved._archived) {
2475
+ inMemSaveBatch.push(resolved);
2476
+ } else {
2477
+ inMemDeleteIds.push(serverItem._id);
2478
+ }
2451
2479
  } else {
2452
- inMemDeleteIds.push(serverItem._id);
2480
+ dexieBatch.push(serverItem);
2481
+ if (!serverItem._deleted && !serverItem._archived) {
2482
+ inMemSaveBatch.push(serverItem);
2483
+ } else {
2484
+ inMemDeleteIds.push(serverItem._id);
2485
+ }
2453
2486
  }
2454
2487
  } else {
2455
2488
  dexieBatch.push(serverItem);
2456
2489
  if (!serverItem._deleted && !serverItem._archived) {
2457
2490
  inMemSaveBatch.push(serverItem);
2458
- } else {
2459
- inMemDeleteIds.push(serverItem._id);
2460
2491
  }
2461
2492
  }
2462
- } else {
2463
- dexieBatch.push(serverItem);
2464
- if (!serverItem._deleted && !serverItem._archived) {
2465
- inMemSaveBatch.push(serverItem);
2466
- }
2467
2493
  }
2468
- }
2469
- if (dexieBatch.length > 0) {
2470
- await this.dexieDb.saveMany(collectionName, dexieBatch);
2471
- }
2472
- if (inMemSaveBatch.length > 0) {
2473
- this.deps.writeToInMemBatch(collectionName, inMemSaveBatch, "upsert");
2474
- }
2475
- if (inMemDeleteIds.length > 0) {
2476
- this.deps.writeToInMemBatch(collectionName, inMemDeleteIds.map((id) => ({ _id: id })), "delete");
2494
+ if (dexieBatch.length > 0) {
2495
+ await this.dexieDb.saveMany(collectionName, dexieBatch);
2496
+ }
2497
+ if (inMemSaveBatch.length > 0) {
2498
+ this.deps.writeToInMemBatch(collectionName, inMemSaveBatch, "upsert");
2499
+ }
2500
+ if (inMemDeleteIds.length > 0) {
2501
+ this.deps.writeToInMemBatch(collectionName, inMemDeleteIds.map((id) => ({ _id: id })), "delete");
2502
+ }
2503
+ for (const id of chunkIds) {
2504
+ allUpdatedIds.push(String(id));
2505
+ }
2477
2506
  }
2478
2507
  if (maxTs) {
2479
2508
  await this.dexieDb.setSyncMeta(collectionName, maxTs);
@@ -2483,8 +2512,7 @@ class SyncEngine {
2483
2512
  lastSyncTs: maxTs
2484
2513
  });
2485
2514
  }
2486
- const updatedIds = serverIds.map((id) => String(id));
2487
- return { conflictsResolved, maxTs, updatedIds };
2515
+ return { conflictsResolved, maxTs, updatedIds: allUpdatedIds };
2488
2516
  }
2489
2517
  compareTimestamps(a, b) {
2490
2518
  const aT = typeof a === "object" && "t" in a ? a.t : 0;
@@ -3251,8 +3279,15 @@ class SyncedDb {
3251
3279
  await this.pendingChanges.recoverPendingWrites();
3252
3280
  for (const [name] of this.collections) {
3253
3281
  const data = await this.dexieDb.getAll(name);
3254
- const activeData = data.filter((item) => !item._deleted && !item._archived);
3255
- this.inMemManager.initCollection(name, activeData);
3282
+ let writeIdx = 0;
3283
+ for (let i = 0;i < data.length; i++) {
3284
+ const item = data[i];
3285
+ if (!item._deleted && !item._archived) {
3286
+ data[writeIdx++] = item;
3287
+ }
3288
+ }
3289
+ data.length = writeIdx;
3290
+ this.inMemManager.initCollection(name, data);
3256
3291
  const meta = await this.dexieDb.getSyncMeta(name);
3257
3292
  if (meta) {
3258
3293
  this.syncMetaCache.set(name, meta);
@@ -6210,6 +6245,12 @@ var pack2 = (x) => packr.pack(preprocessForPack(x));
6210
6245
  var unpack2 = (x) => unpackr.unpack(x);
6211
6246
  var DEFAULT_TIMEOUT = 5000;
6212
6247
  var DEFAULT_PROGRESS_CHUNK_SIZE = 16 * 1024;
6248
+ function concatUint8(a, b) {
6249
+ const result = new Uint8Array(a.length + b.length);
6250
+ result.set(a);
6251
+ result.set(b, a.length);
6252
+ return result;
6253
+ }
6213
6254
 
6214
6255
  class RestProxy {
6215
6256
  endpoint;
@@ -6370,6 +6411,129 @@ class RestProxy {
6370
6411
  async findNewerMany(spec) {
6371
6412
  return await this.restCall("findNewerMany", { spec });
6372
6413
  }
6414
+ async findNewerManyStream(spec, onChunk, options) {
6415
+ const connectTimeout = options?.timeoutMs ?? this.defaultTimeoutMs;
6416
+ const activityTimeout = options?.activityTimeoutMs ?? 30000;
6417
+ const externalSignal = options?.signal ?? this.globalSignal;
6418
+ const startTime = this.timeRequests ? performance.now() : 0;
6419
+ const data = {
6420
+ payload: {
6421
+ db: this.tenant,
6422
+ operation: "findNewerMany",
6423
+ spec
6424
+ },
6425
+ audit: {
6426
+ tenant: this.tenant,
6427
+ user: this.audit.user,
6428
+ naprava: this.audit.device
6429
+ }
6430
+ };
6431
+ const body = pack2(data);
6432
+ const requestUrl = this.apiKey ? `${this.endpoint}?apikey=${this.apiKey}&stream=1` : `${this.endpoint}?stream=1`;
6433
+ const controller = new AbortController;
6434
+ let timeoutId = setTimeout(() => controller.abort(), connectTimeout);
6435
+ const combinedSignal = externalSignal ? this.combineSignals(externalSignal, controller.signal) : controller.signal;
6436
+ try {
6437
+ const response = await fetch(requestUrl, {
6438
+ method: "POST",
6439
+ headers: { "Content-Type": "application/octet-stream" },
6440
+ body,
6441
+ signal: combinedSignal
6442
+ });
6443
+ clearTimeout(timeoutId);
6444
+ timeoutId = undefined;
6445
+ if (!response.ok) {
6446
+ const errorText = await response.text();
6447
+ throw new Error(`REST call failed: ${response.status} - ${errorText}`);
6448
+ }
6449
+ const resetActivity = () => {
6450
+ if (timeoutId !== undefined)
6451
+ clearTimeout(timeoutId);
6452
+ timeoutId = setTimeout(() => controller.abort(), activityTimeout);
6453
+ };
6454
+ resetActivity();
6455
+ await this.parseStreamingResponse(response, onChunk, resetActivity);
6456
+ if (timeoutId !== undefined)
6457
+ clearTimeout(timeoutId);
6458
+ timeoutId = undefined;
6459
+ if (this.timeRequests) {
6460
+ const elapsed = performance.now() - startTime;
6461
+ this._lastRequestMs = elapsed;
6462
+ this._totalRequestMs += elapsed;
6463
+ this._requestCount++;
6464
+ if (this.timeRequestsPrint) {
6465
+ console.log(`[RestProxy] findNewerManyStream: ${elapsed.toFixed(2)}ms (total: ${this._totalRequestMs.toFixed(2)}ms, count: ${this._requestCount})`);
6466
+ }
6467
+ }
6468
+ } catch (err) {
6469
+ if (timeoutId !== undefined)
6470
+ clearTimeout(timeoutId);
6471
+ if (err.name === "AbortError") {
6472
+ if (controller.signal.aborted && !externalSignal?.aborted) {
6473
+ throw new Error(`REST call timeout: findNewerManyStream`);
6474
+ }
6475
+ throw new Error("REST call aborted: findNewerManyStream");
6476
+ }
6477
+ throw err;
6478
+ }
6479
+ }
6480
+ async parseStreamingResponse(response, onChunk, onActivity) {
6481
+ const reader = response.body.getReader();
6482
+ let buffer = new Uint8Array(0);
6483
+ const readMore = async () => {
6484
+ const { done, value } = await reader.read();
6485
+ if (done)
6486
+ return false;
6487
+ onActivity();
6488
+ buffer = concatUint8(buffer, value);
6489
+ return true;
6490
+ };
6491
+ while (buffer.length < 1) {
6492
+ if (!await readMore())
6493
+ return;
6494
+ }
6495
+ const firstByte = buffer[0];
6496
+ if (firstByte !== 0 && firstByte !== 1) {
6497
+ while (await readMore()) {}
6498
+ const result = unpack2(buffer);
6499
+ for (const [collection, items] of Object.entries(result)) {
6500
+ if (items.length > 0) {
6501
+ await onChunk(collection, items);
6502
+ }
6503
+ }
6504
+ return;
6505
+ }
6506
+ while (true) {
6507
+ while (buffer.length < 1) {
6508
+ if (!await readMore())
6509
+ return;
6510
+ }
6511
+ if (buffer[0] === 0)
6512
+ return;
6513
+ while (buffer.length < 3) {
6514
+ if (!await readMore())
6515
+ throw new Error("Unexpected end of stream in chunk header");
6516
+ }
6517
+ const nameLen = buffer[1] << 8 | buffer[2];
6518
+ const headerSize = 1 + 2 + nameLen + 4;
6519
+ while (buffer.length < headerSize) {
6520
+ if (!await readMore())
6521
+ throw new Error("Unexpected end of stream in chunk header");
6522
+ }
6523
+ const collection = new TextDecoder().decode(buffer.slice(3, 3 + nameLen));
6524
+ const dataOffset = 3 + nameLen;
6525
+ const dataLen = buffer[dataOffset] << 24 | buffer[dataOffset + 1] << 16 | buffer[dataOffset + 2] << 8 | buffer[dataOffset + 3];
6526
+ const totalChunkSize = headerSize + dataLen;
6527
+ while (buffer.length < totalChunkSize) {
6528
+ if (!await readMore())
6529
+ throw new Error("Unexpected end of stream in chunk data");
6530
+ }
6531
+ const payloadBytes = buffer.slice(headerSize, totalChunkSize);
6532
+ const items = unpack2(payloadBytes);
6533
+ buffer = buffer.slice(totalChunkSize);
6534
+ await onChunk(collection, items);
6535
+ }
6536
+ }
6373
6537
  async deleteOne(collection, query) {
6374
6538
  return await this.restCall("deleteOne", { collection, query });
6375
6539
  }
@@ -101,6 +101,28 @@ export declare class RestProxy implements I_RestInterface {
101
101
  findByIds<T>(collection: string, ids: Id[]): Promise<T[]>;
102
102
  findNewer<T>(collection: string, timestamp: Timestamp | number | string | Date, query?: QuerySpec<T>, opts?: QueryOpts): Promise<T[]>;
103
103
  findNewerMany<T>(spec?: GetNewerSpec<T>[]): Promise<Record<string, any[]>>;
104
+ /**
105
+ * Streaming variant of findNewerMany.
106
+ * Reads chunked binary response and calls onChunk for each batch.
107
+ * Peak memory = one chunk (~200 docs) instead of entire result set.
108
+ *
109
+ * Binary chunk format:
110
+ * [type:1][nameLen:2][name:N][dataLen:4][msgpack(items[]):M]
111
+ * type=0x01 for data, type=0x00 for end-of-stream.
112
+ */
113
+ findNewerManyStream<T>(spec: GetNewerSpec<T>[], onChunk: (collection: string, items: T[]) => Promise<void>, options?: {
114
+ timeoutMs?: number;
115
+ signal?: AbortSignal;
116
+ activityTimeoutMs?: number;
117
+ }): Promise<void>;
118
+ /**
119
+ * Parse streaming response. Auto-detects format:
120
+ * - Streaming: first byte is 0x00 (end) or 0x01 (data chunk)
121
+ * - Legacy msgpack: first byte is msgpack type marker (0x80+ for map, etc.)
122
+ *
123
+ * Streaming chunk format: [type:1][nameLen:2][name:N][dataLen:4][msgpack(items[]):M]
124
+ */
125
+ private parseStreamingResponse;
104
126
  deleteOne<T>(collection: string, query: QuerySpec<T>): Promise<T>;
105
127
  aggregate<T>(collection: string, pipeline: object[], opts?: AggregateOptions): Promise<T[]>;
106
128
  upsertBatch<T>(collection: string, batch: BatchSpec<T>): Promise<T[]>;
@@ -38,6 +38,8 @@ export declare class SyncEngine implements I_SyncEngine {
38
38
  processCollectionServerData(collectionName: string, serverData: LocalDbEntity[]): Promise<{
39
39
  updatedIds: string[];
40
40
  }>;
41
+ /** Max items to process per batch in processIncomingServerData */
42
+ private static readonly SYNC_BATCH_SIZE;
41
43
  private processIncomingServerData;
42
44
  private compareTimestamps;
43
45
  private resolveCollectionConflict;
@@ -67,6 +67,11 @@ export interface I_RestInterface {
67
67
  findByIds<T>(collection: string, ids: Id[]): Promise<T[]>;
68
68
  findNewer<T>(collection: string, timestamp: Timestamp | number | string | Date, query?: QuerySpec<T>, opts?: QueryOpts): Promise<T[]>;
69
69
  findNewerMany<T>(spec?: GetNewerSpec<T>[]): Promise<Record<string, any[]>>;
70
+ /** Streaming variant of findNewerMany. Calls onChunk for each batch of items as they arrive. */
71
+ findNewerManyStream<T>(spec: GetNewerSpec<T>[], onChunk: (collection: string, items: T[]) => Promise<void>, options?: {
72
+ timeoutMs?: number;
73
+ signal?: AbortSignal;
74
+ }): Promise<void>;
70
75
  deleteOne<T>(collection: string, query: QuerySpec<T>): Promise<T>;
71
76
  /** Izvede agregacijo na serverju */
72
77
  aggregate<T>(collection: string, pipeline: object[], opts?: AggregateOptions): Promise<T[]>;
@@ -206,7 +206,7 @@ export interface CollectionSyncStats {
206
206
  receivedCount: number;
207
207
  /** Number of dirty items sent to server for this collection */
208
208
  sentCount: number;
209
- /** The actual items received from server (for debugging/logging) */
209
+ /** @deprecated Use receivedCount instead. Will be empty array in future streaming mode. */
210
210
  receivedItems: LocalDbEntity[];
211
211
  }
212
212
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",