cry-synced-db-client 0.1.72 → 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,30 +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
- delete allServerData[collectionName];
2170
- receivedCount += serverData.length;
2171
- collectionStats[collectionName] = {
2172
- receivedCount: serverData.length,
2173
- sentCount: 0,
2174
- receivedItems: []
2175
- };
2176
- const stats = await this.processIncomingServerData(collectionName, config, serverData);
2177
- conflictsResolved += stats.conflictsResolved;
2178
- if (stats.updatedIds.length > 0) {
2179
- allUpdatedIds[collectionName] = stats.updatedIds;
2180
- }
2181
- }
2182
2201
  if (Object.keys(allUpdatedIds).length > 0) {
2183
2202
  this.deps.broadcastUpdates(allUpdatedIds);
2184
2203
  }
@@ -6226,6 +6245,12 @@ var pack2 = (x) => packr.pack(preprocessForPack(x));
6226
6245
  var unpack2 = (x) => unpackr.unpack(x);
6227
6246
  var DEFAULT_TIMEOUT = 5000;
6228
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
+ }
6229
6254
 
6230
6255
  class RestProxy {
6231
6256
  endpoint;
@@ -6386,6 +6411,129 @@ class RestProxy {
6386
6411
  async findNewerMany(spec) {
6387
6412
  return await this.restCall("findNewerMany", { spec });
6388
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
+ }
6389
6537
  async deleteOne(collection, query) {
6390
6538
  return await this.restCall("deleteOne", { collection, query });
6391
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[]>;
@@ -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[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.72",
3
+ "version": "0.1.73",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",