meadow-integration 1.0.12 → 1.0.14
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/package.json
CHANGED
|
@@ -42,6 +42,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
42
42
|
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
43
43
|
this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
|
|
44
44
|
|
|
45
|
+
// Minimum range size for bisection -- when a range is this small or smaller,
|
|
46
|
+
// pull all records in the range from the server instead of subdividing further.
|
|
47
|
+
this.BisectMinRangeSize = this.options.BisectMinRangeSize || 1000;
|
|
48
|
+
|
|
49
|
+
// Tolerance window in milliseconds for cross-database timestamp precision differences.
|
|
50
|
+
// MySQL DATETIME stores whole seconds, MSSQL DATETIME rounds to ~3.33ms increments,
|
|
51
|
+
// PostgreSQL TIMESTAMP stores microseconds, and SQLite stores as TEXT. When comparing
|
|
52
|
+
// timestamps across systems, the maximum rounding error is 1000ms (MySQL second-level
|
|
53
|
+
// truncation). Default 1000ms covers all supported provider combinations.
|
|
54
|
+
this.DateTimePrecisionMS = (typeof(this.options.DateTimePrecisionMS) === 'number')
|
|
55
|
+
? this.options.DateTimePrecisionMS
|
|
56
|
+
: 1000;
|
|
57
|
+
|
|
45
58
|
this.Meadow = false;
|
|
46
59
|
|
|
47
60
|
this.operation = new libMeadowOperation(this.fable);
|
|
@@ -144,6 +157,460 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
144
157
|
return tmpRecordToCommit;
|
|
145
158
|
}
|
|
146
159
|
|
|
160
|
+
// ---- REST / Local query helpers ----
|
|
161
|
+
|
|
162
|
+
// Normalize a date value to a UTC dayJS instance.
|
|
163
|
+
//
|
|
164
|
+
// Local dates stored in SQLite are in UTC but formatted without a timezone
|
|
165
|
+
// indicator (e.g. "2022-05-10 22:50:26.000"). When the driver or ORM wraps
|
|
166
|
+
// them in a JavaScript Date object, JS interprets them as local time, shifting
|
|
167
|
+
// the value by the machine's UTC offset. To recover the original naive UTC
|
|
168
|
+
// time, we format through local time (which undoes the offset) then re-parse
|
|
169
|
+
// as UTC. Server dates already carry a "Z" suffix and are parsed correctly.
|
|
170
|
+
_normalizeDateUTC(pDate)
|
|
171
|
+
{
|
|
172
|
+
if (typeof(pDate) === 'string')
|
|
173
|
+
{
|
|
174
|
+
// String dates — strip any trailing Z or timezone so dayJS.utc() treats as UTC
|
|
175
|
+
return this.fable.Dates.dayJS.utc(pDate);
|
|
176
|
+
}
|
|
177
|
+
// Date objects (from SQLite via ORM) — format as local time string to recover
|
|
178
|
+
// the naive stored value, then re-parse as UTC
|
|
179
|
+
let tmpNaiveStr = this.fable.Dates.dayJS(pDate).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
180
|
+
return this.fable.Dates.dayJS.utc(tmpNaiveStr);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Format a date value for use in Meadow REST filter expressions (FBV).
|
|
184
|
+
_formatDateForFilter(pDate)
|
|
185
|
+
{
|
|
186
|
+
return this._normalizeDateUTC(pDate).format('YYYY-MM-DDTHH:mm:ss.SSS');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get a count from the remote server, optionally filtered.
|
|
190
|
+
_getServerCount(pFilter, fCallback)
|
|
191
|
+
{
|
|
192
|
+
const tmpURL = pFilter
|
|
193
|
+
? `${this.EntitySchema.TableName}s/Count/FilteredTo/${pFilter}`
|
|
194
|
+
: `${this.EntitySchema.TableName}s/Count`;
|
|
195
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
196
|
+
(pError, pResponse, pBody) =>
|
|
197
|
+
{
|
|
198
|
+
if (pError)
|
|
199
|
+
{
|
|
200
|
+
return fCallback(pError);
|
|
201
|
+
}
|
|
202
|
+
if (pBody && pBody.hasOwnProperty('Count'))
|
|
203
|
+
{
|
|
204
|
+
return fCallback(null, pBody.Count);
|
|
205
|
+
}
|
|
206
|
+
return fCallback(null, 0);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get a page of records from the remote server with a Meadow filter expression.
|
|
211
|
+
_getServerRecords(pFilter, pOffset, pPageSize, fCallback)
|
|
212
|
+
{
|
|
213
|
+
const tmpURL = `${this.EntitySchema.TableName}s/FilteredTo/${pFilter}/${pOffset}/${pPageSize}`;
|
|
214
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
215
|
+
(pError, pResponse, pBody) =>
|
|
216
|
+
{
|
|
217
|
+
if (pError)
|
|
218
|
+
{
|
|
219
|
+
return fCallback(pError);
|
|
220
|
+
}
|
|
221
|
+
if (pBody && Array.isArray(pBody))
|
|
222
|
+
{
|
|
223
|
+
return fCallback(null, pBody);
|
|
224
|
+
}
|
|
225
|
+
return fCallback(null, []);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get a count from the local database with optional ID range filters.
|
|
230
|
+
_getLocalCount(pMinID, pMaxID, fCallback)
|
|
231
|
+
{
|
|
232
|
+
const tmpQuery = this.Meadow.query;
|
|
233
|
+
if (pMinID > 0)
|
|
234
|
+
{
|
|
235
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
236
|
+
}
|
|
237
|
+
if (pMaxID > 0)
|
|
238
|
+
{
|
|
239
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
240
|
+
}
|
|
241
|
+
if (!this._hasDeletedColumn)
|
|
242
|
+
{
|
|
243
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
244
|
+
}
|
|
245
|
+
this.Meadow.doCount(tmpQuery,
|
|
246
|
+
(pError, pQuery, pCount) =>
|
|
247
|
+
{
|
|
248
|
+
if (pError)
|
|
249
|
+
{
|
|
250
|
+
return fCallback(pError);
|
|
251
|
+
}
|
|
252
|
+
return fCallback(null, pCount);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get the max UpdateDate from local records in an ID range.
|
|
257
|
+
_getLocalMaxUpdateDate(pMinID, pMaxID, fCallback)
|
|
258
|
+
{
|
|
259
|
+
const tmpQuery = this.Meadow.query;
|
|
260
|
+
if (pMinID > 0)
|
|
261
|
+
{
|
|
262
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
263
|
+
}
|
|
264
|
+
if (pMaxID > 0)
|
|
265
|
+
{
|
|
266
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
267
|
+
}
|
|
268
|
+
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
269
|
+
tmpQuery.setCap(1);
|
|
270
|
+
if (!this._hasDeletedColumn)
|
|
271
|
+
{
|
|
272
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
273
|
+
}
|
|
274
|
+
this.Meadow.doRead(tmpQuery,
|
|
275
|
+
(pError, pQuery, pRecord) =>
|
|
276
|
+
{
|
|
277
|
+
if (pError)
|
|
278
|
+
{
|
|
279
|
+
return fCallback(pError);
|
|
280
|
+
}
|
|
281
|
+
if (!pRecord || !pRecord.UpdateDate)
|
|
282
|
+
{
|
|
283
|
+
return fCallback(null, false);
|
|
284
|
+
}
|
|
285
|
+
return fCallback(null, pRecord.UpdateDate);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Get the min UpdateDate from local records in an ID range.
|
|
290
|
+
_getLocalMinUpdateDate(pMinID, pMaxID, fCallback)
|
|
291
|
+
{
|
|
292
|
+
const tmpQuery = this.Meadow.query;
|
|
293
|
+
if (pMinID > 0)
|
|
294
|
+
{
|
|
295
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
296
|
+
}
|
|
297
|
+
if (pMaxID > 0)
|
|
298
|
+
{
|
|
299
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
300
|
+
}
|
|
301
|
+
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Ascending' });
|
|
302
|
+
tmpQuery.setCap(1);
|
|
303
|
+
if (!this._hasDeletedColumn)
|
|
304
|
+
{
|
|
305
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
306
|
+
}
|
|
307
|
+
this.Meadow.doRead(tmpQuery,
|
|
308
|
+
(pError, pQuery, pRecord) =>
|
|
309
|
+
{
|
|
310
|
+
if (pError)
|
|
311
|
+
{
|
|
312
|
+
return fCallback(pError);
|
|
313
|
+
}
|
|
314
|
+
if (!pRecord || !pRecord.UpdateDate)
|
|
315
|
+
{
|
|
316
|
+
return fCallback(null, false);
|
|
317
|
+
}
|
|
318
|
+
return fCallback(null, pRecord.UpdateDate);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Upsert a single record from the server into the local database.
|
|
323
|
+
_upsertRecord(pServerRecord, fCallback)
|
|
324
|
+
{
|
|
325
|
+
const tmpRecordToCommit = this.marshalRecord(pServerRecord);
|
|
326
|
+
|
|
327
|
+
const tmpQuery = this.Meadow.query;
|
|
328
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pServerRecord[this.DefaultIdentifier]);
|
|
329
|
+
if (!this._hasDeletedColumn)
|
|
330
|
+
{
|
|
331
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.Meadow.doRead(tmpQuery,
|
|
335
|
+
(pReadError, pQuery, pLocalRecord) =>
|
|
336
|
+
{
|
|
337
|
+
if (pReadError)
|
|
338
|
+
{
|
|
339
|
+
this.fable.log.error(`Error reading local record ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pReadError}`);
|
|
340
|
+
return fCallback();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
344
|
+
tmpSyncQuery.setDisableAutoIdentity(true);
|
|
345
|
+
tmpSyncQuery.setDisableAutoDateStamp(true);
|
|
346
|
+
tmpSyncQuery.setDisableAutoUserStamp(true);
|
|
347
|
+
tmpSyncQuery.setDisableDeleteTracking(true);
|
|
348
|
+
|
|
349
|
+
if (!pLocalRecord)
|
|
350
|
+
{
|
|
351
|
+
// Record does not exist locally -- create
|
|
352
|
+
tmpSyncQuery.AllowIdentityInsert = true;
|
|
353
|
+
this.Meadow.doCreate(tmpSyncQuery,
|
|
354
|
+
(pCreateError) =>
|
|
355
|
+
{
|
|
356
|
+
if (pCreateError)
|
|
357
|
+
{
|
|
358
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
359
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
360
|
+
{
|
|
361
|
+
// GUID conflict -- fall back to update
|
|
362
|
+
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
363
|
+
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
364
|
+
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
365
|
+
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
366
|
+
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
367
|
+
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
368
|
+
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
369
|
+
(pUpdateError) =>
|
|
370
|
+
{
|
|
371
|
+
if (pUpdateError)
|
|
372
|
+
{
|
|
373
|
+
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
374
|
+
}
|
|
375
|
+
return fCallback();
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
380
|
+
return fCallback();
|
|
381
|
+
}
|
|
382
|
+
return fCallback();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else
|
|
386
|
+
{
|
|
387
|
+
// Record exists locally -- update
|
|
388
|
+
this.Meadow.doUpdate(tmpSyncQuery,
|
|
389
|
+
(pUpdateError) =>
|
|
390
|
+
{
|
|
391
|
+
if (pUpdateError)
|
|
392
|
+
{
|
|
393
|
+
this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
|
|
394
|
+
}
|
|
395
|
+
return fCallback();
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Pull all records from server matching a filter expression and upsert them locally.
|
|
402
|
+
// Fetches in pages of this.PageSize.
|
|
403
|
+
_pullServerRecords(pFilter, pEstimatedCount, fCallback)
|
|
404
|
+
{
|
|
405
|
+
if (pEstimatedCount < 1)
|
|
406
|
+
{
|
|
407
|
+
return fCallback(null, 0);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let tmpSyncedCount = 0;
|
|
411
|
+
let tmpOffset = 0;
|
|
412
|
+
let tmpDone = false;
|
|
413
|
+
|
|
414
|
+
let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
|
|
415
|
+
? Math.min(pEstimatedCount, this.MaxRecordsPerEntity)
|
|
416
|
+
: pEstimatedCount;
|
|
417
|
+
|
|
418
|
+
const fFetchPage = () =>
|
|
419
|
+
{
|
|
420
|
+
if (tmpDone || tmpOffset >= tmpRecordCap)
|
|
421
|
+
{
|
|
422
|
+
return fCallback(null, tmpSyncedCount);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this._getServerRecords(pFilter, tmpOffset, this.PageSize,
|
|
426
|
+
(pError, pRecords) =>
|
|
427
|
+
{
|
|
428
|
+
if (pError)
|
|
429
|
+
{
|
|
430
|
+
this.fable.log.error(`Error fetching ${this.EntitySchema.TableName} page at offset ${tmpOffset}: ${pError}`);
|
|
431
|
+
return fCallback(pError, tmpSyncedCount);
|
|
432
|
+
}
|
|
433
|
+
if (!pRecords || pRecords.length < 1)
|
|
434
|
+
{
|
|
435
|
+
tmpDone = true;
|
|
436
|
+
return fCallback(null, tmpSyncedCount);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.fable.Utility.eachLimit(pRecords, 5,
|
|
440
|
+
(pRecord, fRecordDone) =>
|
|
441
|
+
{
|
|
442
|
+
this._upsertRecord(pRecord,
|
|
443
|
+
() =>
|
|
444
|
+
{
|
|
445
|
+
tmpSyncedCount++;
|
|
446
|
+
return fRecordDone();
|
|
447
|
+
});
|
|
448
|
+
},
|
|
449
|
+
(pUpsertError) =>
|
|
450
|
+
{
|
|
451
|
+
tmpOffset += this.PageSize;
|
|
452
|
+
if (pRecords.length < this.PageSize)
|
|
453
|
+
{
|
|
454
|
+
tmpDone = true;
|
|
455
|
+
return fCallback(null, tmpSyncedCount);
|
|
456
|
+
}
|
|
457
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${tmpSyncedCount} of ~${tmpRecordCap} records...`);
|
|
458
|
+
return fFetchPage();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
fFetchPage();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Increment the FullSync progress tracker by pCount records checked/synced.
|
|
467
|
+
_incrementProgress(pCount)
|
|
468
|
+
{
|
|
469
|
+
let tmpTracker = this.operation.progressTrackers[`FullSync-${this.EntitySchema.TableName}`];
|
|
470
|
+
if (tmpTracker && pCount > 0)
|
|
471
|
+
{
|
|
472
|
+
tmpTracker.CurrentCount = Math.min(tmpTracker.CurrentCount + pCount, tmpTracker.TotalCount);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ---- Bisection logic ----
|
|
477
|
+
|
|
478
|
+
// Compare a local ID range against the server. If counts or date boundaries
|
|
479
|
+
// differ, subdivide until the range is small enough, then pull all records in
|
|
480
|
+
// the range from the server to bring local in sync.
|
|
481
|
+
_bisectRange(pMinID, pMaxID, pDepth, fCallback)
|
|
482
|
+
{
|
|
483
|
+
const tmpRangeSize = pMaxID - pMinID + 1;
|
|
484
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
485
|
+
const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
|
|
486
|
+
|
|
487
|
+
// Get local stats for this range
|
|
488
|
+
this._getLocalCount(pMinID, pMaxID,
|
|
489
|
+
(pLocalCountError, pLocalCount) =>
|
|
490
|
+
{
|
|
491
|
+
if (pLocalCountError)
|
|
492
|
+
{
|
|
493
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect local count error for range ${pMinID}-${pMaxID}: ${pLocalCountError}`);
|
|
494
|
+
return fCallback();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Get server count for this range
|
|
498
|
+
this._getServerCount(tmpRangeFilter,
|
|
499
|
+
(pServerCountError, pServerCount) =>
|
|
500
|
+
{
|
|
501
|
+
if (pServerCountError)
|
|
502
|
+
{
|
|
503
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect server count error for range ${pMinID}-${pMaxID}: ${pServerCountError}`);
|
|
504
|
+
return fCallback();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// If counts match, check UpdateDate boundaries for this range
|
|
508
|
+
if (pLocalCount === pServerCount)
|
|
509
|
+
{
|
|
510
|
+
if (!this._hasUpdateDate)
|
|
511
|
+
{
|
|
512
|
+
// No UpdateDate column -- counts match, assume in sync
|
|
513
|
+
this._incrementProgress(pServerCount);
|
|
514
|
+
return fCallback();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Compare max and min UpdateDate for this range
|
|
518
|
+
this._getLocalMaxUpdateDate(pMinID, pMaxID,
|
|
519
|
+
(pLocalMaxErr, pLocalMaxDate) =>
|
|
520
|
+
{
|
|
521
|
+
if (pLocalMaxErr || !pLocalMaxDate)
|
|
522
|
+
{
|
|
523
|
+
return fCallback();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Get server max UpdateDate for this range (1 record, sorted desc)
|
|
527
|
+
const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
|
|
528
|
+
this._getServerRecords(tmpMaxDateFilter, 0, 1,
|
|
529
|
+
(pServerMaxErr, pServerMaxRecords) =>
|
|
530
|
+
{
|
|
531
|
+
if (pServerMaxErr || !pServerMaxRecords || pServerMaxRecords.length < 1)
|
|
532
|
+
{
|
|
533
|
+
return fCallback();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const tmpServerMaxDate = pServerMaxRecords[0].UpdateDate;
|
|
537
|
+
const tmpMaxDateDiff = Math.abs(this._normalizeDateUTC(pLocalMaxDate).diff(this._normalizeDateUTC(tmpServerMaxDate)));
|
|
538
|
+
|
|
539
|
+
if (tmpMaxDateDiff <= this.DateTimePrecisionMS)
|
|
540
|
+
{
|
|
541
|
+
// Max dates match and counts match -- this range is in sync
|
|
542
|
+
this._incrementProgress(pServerCount);
|
|
543
|
+
return fCallback();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Dates differ even though counts match -- records have been modified.
|
|
547
|
+
// If range is small enough, pull all records; otherwise subdivide.
|
|
548
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: date mismatch in range ${pMinID}-${pMaxID} (local max: ${pLocalMaxDate}, server max: ${tmpServerMaxDate})`);
|
|
549
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
550
|
+
{
|
|
551
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
552
|
+
}
|
|
553
|
+
return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Counts differ
|
|
560
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch in range ${pMinID}-${pMaxID} (local: ${pLocalCount}, server: ${pServerCount})`);
|
|
561
|
+
|
|
562
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
563
|
+
{
|
|
564
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Split an ID range in half and bisect each half.
|
|
573
|
+
_subdivideRange(pMinID, pMaxID, pDepth, fCallback)
|
|
574
|
+
{
|
|
575
|
+
const tmpMidID = Math.floor((pMinID + pMaxID) / 2);
|
|
576
|
+
|
|
577
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: subdividing range ${pMinID}-${pMaxID} at ID ${tmpMidID} (depth ${pDepth})`);
|
|
578
|
+
|
|
579
|
+
// Bisect lower half, then upper half
|
|
580
|
+
this._bisectRange(pMinID, tmpMidID, pDepth + 1,
|
|
581
|
+
() =>
|
|
582
|
+
{
|
|
583
|
+
this._bisectRange(tmpMidID + 1, pMaxID, pDepth + 1, fCallback);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Pull all records from the server in an ID range and upsert them locally.
|
|
588
|
+
_pullRangeFromServer(pMinID, pMaxID, fCallback)
|
|
589
|
+
{
|
|
590
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
591
|
+
const tmpFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
592
|
+
const tmpEstimatedCount = pMaxID - pMinID + 1;
|
|
593
|
+
|
|
594
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling range ${pMinID}-${pMaxID} from server (~${tmpEstimatedCount} records)`);
|
|
595
|
+
|
|
596
|
+
this._pullServerRecords(tmpFilter, tmpEstimatedCount,
|
|
597
|
+
(pError, pSyncedCount) =>
|
|
598
|
+
{
|
|
599
|
+
if (pError)
|
|
600
|
+
{
|
|
601
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling range ${pMinID}-${pMaxID}: ${pError}`);
|
|
602
|
+
}
|
|
603
|
+
else
|
|
604
|
+
{
|
|
605
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: synced ${pSyncedCount} records in range ${pMinID}-${pMaxID}`);
|
|
606
|
+
}
|
|
607
|
+
this._incrementProgress(pSyncedCount || 0);
|
|
608
|
+
return fCallback();
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ---- Deleted records sync ----
|
|
613
|
+
|
|
147
614
|
syncDeletedRecords(fCallback)
|
|
148
615
|
{
|
|
149
616
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
@@ -155,7 +622,6 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
155
622
|
|
|
156
623
|
this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
|
|
157
624
|
|
|
158
|
-
// Get the count of deleted records from the server.
|
|
159
625
|
// The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
|
|
160
626
|
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
|
|
161
627
|
(pError, pResponse, pBody) =>
|
|
@@ -277,149 +743,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
277
743
|
});
|
|
278
744
|
}
|
|
279
745
|
|
|
280
|
-
|
|
281
|
-
{
|
|
282
|
-
tmpAnticipate.anticipate(
|
|
283
|
-
(fNext) =>
|
|
284
|
-
{
|
|
285
|
-
const tmpURLPartial = `${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.LastRequestedID}~FSF~${this.DefaultIdentifier}~ASC~ASC/0/${this.PageSize}`;
|
|
286
|
-
this.fable.MeadowCloneRestClient.getJSON(tmpURLPartial,
|
|
287
|
-
(pDownloadError, pResponse, pBody) =>
|
|
288
|
-
{
|
|
289
|
-
if (pDownloadError)
|
|
290
|
-
{
|
|
291
|
-
this.fable.log.error(`Error getting URL Partial [${tmpURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
|
|
292
|
-
return fNext();
|
|
293
|
-
}
|
|
294
|
-
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
295
|
-
{
|
|
296
|
-
for (let i = 0; i < pBody.length; i++)
|
|
297
|
-
{
|
|
298
|
-
const tmpRecord = pBody[i];
|
|
299
|
-
|
|
300
|
-
tmpAnticipate.anticipate(
|
|
301
|
-
(fNextEntityRecordSync) =>
|
|
302
|
-
{
|
|
303
|
-
const tmpQuery = this.Meadow.query;
|
|
304
|
-
|
|
305
|
-
if (tmpRecord[this.DefaultIdentifier] > tmpSyncState.LastRequestedID)
|
|
306
|
-
{
|
|
307
|
-
tmpSyncState.LastRequestedID = tmpRecord[this.DefaultIdentifier];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
|
|
311
|
-
{
|
|
312
|
-
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (!tmpSyncState.HasDeletedColumn)
|
|
316
|
-
{
|
|
317
|
-
tmpQuery.setDisableDeleteTracking(true);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
this.Meadow.doRead(tmpQuery,
|
|
321
|
-
(pReadError, pQuery, pRecord) =>
|
|
322
|
-
{
|
|
323
|
-
if (pReadError)
|
|
324
|
-
{
|
|
325
|
-
this.fable.log.error(`Error reading record ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError, PassedRecord: tmpRecord });
|
|
326
|
-
return fNextEntityRecordSync();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (pRecord)
|
|
330
|
-
{
|
|
331
|
-
const tmpAgeDifference = this.fable.Dates.dayJS(tmpRecord.UpdateDate).diff(this.fable.Dates.dayJS(pRecord.UpdateDate));
|
|
332
|
-
|
|
333
|
-
if (Math.abs(tmpAgeDifference) < 5)
|
|
334
|
-
{
|
|
335
|
-
return fNextEntityRecordSync();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
this.fable.log.info(`Syncing ${this.EntitySchema.TableName} record ${tmpRecord[this.DefaultIdentifier]} with age difference of ${tmpAgeDifference} ms.`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const tmpRecordToCommit = this.marshalRecord(tmpRecord);
|
|
342
|
-
|
|
343
|
-
const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
344
|
-
|
|
345
|
-
tmpSyncQuery.setDisableAutoIdentity(true);
|
|
346
|
-
tmpSyncQuery.setDisableAutoDateStamp(true);
|
|
347
|
-
tmpSyncQuery.setDisableAutoUserStamp(true);
|
|
348
|
-
tmpSyncQuery.setDisableDeleteTracking(true);
|
|
349
|
-
|
|
350
|
-
if (!pRecord)
|
|
351
|
-
{
|
|
352
|
-
// Record not found -- create
|
|
353
|
-
tmpSyncQuery.AllowIdentityInsert = true;
|
|
354
|
-
|
|
355
|
-
this.Meadow.doCreate(tmpSyncQuery,
|
|
356
|
-
(pCreateError) =>
|
|
357
|
-
{
|
|
358
|
-
if (pCreateError)
|
|
359
|
-
{
|
|
360
|
-
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
361
|
-
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
362
|
-
{
|
|
363
|
-
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
364
|
-
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
365
|
-
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
366
|
-
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
367
|
-
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
368
|
-
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
369
|
-
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
370
|
-
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
371
|
-
(pUpdateError) =>
|
|
372
|
-
{
|
|
373
|
-
if (pUpdateError)
|
|
374
|
-
{
|
|
375
|
-
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
376
|
-
return fNextEntityRecordSync();
|
|
377
|
-
}
|
|
378
|
-
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
379
|
-
return fNextEntityRecordSync();
|
|
380
|
-
});
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
384
|
-
return fNextEntityRecordSync();
|
|
385
|
-
}
|
|
386
|
-
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
387
|
-
return fNextEntityRecordSync();
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
else
|
|
391
|
-
{
|
|
392
|
-
// Record found -- update
|
|
393
|
-
this.Meadow.doUpdate(tmpSyncQuery,
|
|
394
|
-
(pUpdateError) =>
|
|
395
|
-
{
|
|
396
|
-
if (pUpdateError)
|
|
397
|
-
{
|
|
398
|
-
this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
|
|
399
|
-
return fNextEntityRecordSync();
|
|
400
|
-
}
|
|
401
|
-
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
402
|
-
return fNextEntityRecordSync();
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
tmpSyncState.RequestsPerformed++;
|
|
409
|
-
if (tmpSyncState.RequestsPerformed < tmpSyncState.EstimatedRequestCount)
|
|
410
|
-
{
|
|
411
|
-
this.fable.log.info(`Syncing ${this.EntitySchema.TableName} request ${tmpSyncState.RequestsPerformed} of ${tmpSyncState.EstimatedRequestCount}...`);
|
|
412
|
-
this.addSyncAnticipateEntry(tmpSyncState, tmpAnticipate);
|
|
413
|
-
}
|
|
414
|
-
return fNext();
|
|
415
|
-
}
|
|
416
|
-
else
|
|
417
|
-
{
|
|
418
|
-
return fNext();
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
746
|
+
// ---- Main sync entry point ----
|
|
423
747
|
|
|
424
748
|
sync(fCallback)
|
|
425
749
|
{
|
|
@@ -454,53 +778,44 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
454
778
|
{
|
|
455
779
|
this.operation.createTimeStamp('EntityOngoingSync');
|
|
456
780
|
|
|
457
|
-
let tmpAnticipate = this.fable.newAnticipate();
|
|
458
|
-
|
|
459
781
|
const tmpSyncState = (
|
|
460
782
|
{
|
|
461
|
-
Local: { MaxIDEntity: -1, RecordCount: 0
|
|
462
|
-
Server: { MaxIDEntity: -1, RecordCount: 0
|
|
783
|
+
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
784
|
+
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
463
785
|
});
|
|
464
786
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
{
|
|
469
|
-
if (!this.EntitySchema || !this.EntitySchema.MeadowSchema || !Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
470
|
-
{
|
|
471
|
-
return fStageComplete('MeadowSyncEntityOngoing requires a valid MeadowEntitySchema.MeadowSchema.Schema.');
|
|
472
|
-
}
|
|
787
|
+
// Detect schema capabilities
|
|
788
|
+
this._hasUpdateDate = false;
|
|
789
|
+
this._hasDeletedColumn = false;
|
|
473
790
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
791
|
+
if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
792
|
+
{
|
|
793
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
794
|
+
{
|
|
795
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
796
|
+
if (tmpColumn.Column == 'UpdateDate')
|
|
797
|
+
{
|
|
798
|
+
this._hasUpdateDate = true;
|
|
799
|
+
}
|
|
800
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
801
|
+
{
|
|
802
|
+
this._hasDeletedColumn = true;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
487
806
|
|
|
488
|
-
|
|
489
|
-
{
|
|
490
|
-
this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
|
|
491
|
-
}
|
|
807
|
+
this.fable.log.info(`Syncing with ONGOING STRATEGY entity ${this.EntitySchema.TableName} (UpdateDate: ${this._hasUpdateDate}, Deleted: ${this._hasDeletedColumn})...`);
|
|
492
808
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
809
|
+
this.fable.Utility.waterfall(
|
|
810
|
+
[
|
|
811
|
+
// ---- Stage 1: Gather local stats ----
|
|
496
812
|
(fStageComplete) =>
|
|
497
813
|
{
|
|
498
|
-
//
|
|
814
|
+
// Local max ID
|
|
499
815
|
const tmpQuery = this.Meadow.query;
|
|
500
816
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
501
817
|
tmpQuery.setCap(1);
|
|
502
|
-
|
|
503
|
-
if (!tmpSyncState.HasDeletedColumn)
|
|
818
|
+
if (!this._hasDeletedColumn)
|
|
504
819
|
{
|
|
505
820
|
tmpQuery.setDisableDeleteTracking(true);
|
|
506
821
|
}
|
|
@@ -509,31 +824,32 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
509
824
|
{
|
|
510
825
|
if (pReadError)
|
|
511
826
|
{
|
|
512
|
-
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}
|
|
827
|
+
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
513
828
|
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
514
829
|
}
|
|
515
|
-
if (
|
|
830
|
+
if (pRecord)
|
|
516
831
|
{
|
|
517
|
-
|
|
518
|
-
|
|
832
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
833
|
+
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxIDEntity}`);
|
|
834
|
+
}
|
|
835
|
+
else
|
|
836
|
+
{
|
|
837
|
+
this.fable.log.info(`No local records for ${this.EntitySchema.TableName}.`);
|
|
519
838
|
}
|
|
520
|
-
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
|
|
521
|
-
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
522
839
|
return fStageComplete();
|
|
523
840
|
});
|
|
524
841
|
},
|
|
525
842
|
(fStageComplete) =>
|
|
526
843
|
{
|
|
527
|
-
//
|
|
528
|
-
if (!
|
|
844
|
+
// Local max UpdateDate
|
|
845
|
+
if (!this._hasUpdateDate)
|
|
529
846
|
{
|
|
530
|
-
this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
|
|
531
847
|
return fStageComplete();
|
|
532
848
|
}
|
|
533
849
|
const tmpQuery = this.Meadow.query;
|
|
534
850
|
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
535
851
|
tmpQuery.setCap(1);
|
|
536
|
-
if (!
|
|
852
|
+
if (!this._hasDeletedColumn)
|
|
537
853
|
{
|
|
538
854
|
tmpQuery.setDisableDeleteTracking(true);
|
|
539
855
|
}
|
|
@@ -542,24 +858,22 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
542
858
|
{
|
|
543
859
|
if (pReadError)
|
|
544
860
|
{
|
|
545
|
-
this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}
|
|
861
|
+
this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
546
862
|
return fStageComplete(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
547
863
|
}
|
|
548
|
-
if (
|
|
864
|
+
if (pRecord && pRecord.UpdateDate)
|
|
549
865
|
{
|
|
550
|
-
|
|
551
|
-
|
|
866
|
+
tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
|
|
867
|
+
this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxUpdateDate}`);
|
|
552
868
|
}
|
|
553
|
-
this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${pRecord.UpdateDate}`);
|
|
554
|
-
tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
|
|
555
869
|
return fStageComplete();
|
|
556
870
|
});
|
|
557
871
|
},
|
|
558
872
|
(fStageComplete) =>
|
|
559
873
|
{
|
|
560
|
-
//
|
|
874
|
+
// Local count
|
|
561
875
|
const tmpQuery = this.Meadow.query;
|
|
562
|
-
if (!
|
|
876
|
+
if (!this._hasDeletedColumn)
|
|
563
877
|
{
|
|
564
878
|
tmpQuery.setDisableDeleteTracking(true);
|
|
565
879
|
}
|
|
@@ -568,16 +882,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
568
882
|
{
|
|
569
883
|
if (pCountError)
|
|
570
884
|
{
|
|
571
|
-
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}
|
|
885
|
+
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
572
886
|
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
573
887
|
}
|
|
574
888
|
tmpSyncState.Local.RecordCount = pCount;
|
|
889
|
+
this.fable.log.info(`Local count ${this.EntitySchema.TableName}: ${tmpSyncState.Local.RecordCount}`);
|
|
575
890
|
return fStageComplete();
|
|
576
891
|
});
|
|
577
892
|
},
|
|
893
|
+
|
|
894
|
+
// ---- Stage 2: Gather server stats ----
|
|
578
895
|
(fStageComplete) =>
|
|
579
896
|
{
|
|
580
|
-
//
|
|
897
|
+
// Server max ID
|
|
581
898
|
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
|
|
582
899
|
(pError, pResponse, pBody) =>
|
|
583
900
|
{
|
|
@@ -588,8 +905,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
588
905
|
}
|
|
589
906
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
590
907
|
{
|
|
591
|
-
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
|
|
592
908
|
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
909
|
+
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Server.MaxIDEntity}`);
|
|
593
910
|
}
|
|
594
911
|
else
|
|
595
912
|
{
|
|
@@ -600,85 +917,210 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
600
917
|
},
|
|
601
918
|
(fStageComplete) =>
|
|
602
919
|
{
|
|
603
|
-
//
|
|
604
|
-
this.
|
|
605
|
-
(pError,
|
|
920
|
+
// Server count
|
|
921
|
+
this._getServerCount(null,
|
|
922
|
+
(pError, pCount) =>
|
|
606
923
|
{
|
|
607
924
|
if (pError)
|
|
608
925
|
{
|
|
609
|
-
this.fable.log.warn(`Could not get server
|
|
926
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
927
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
610
928
|
return fStageComplete();
|
|
611
929
|
}
|
|
612
|
-
|
|
613
|
-
{
|
|
614
|
-
this.fable.log.info(`Found server max UpdateDate ${this.EntitySchema.TableName}: ${pBody['UpdateDate']}`);
|
|
615
|
-
tmpSyncState.Server.MaxUpdateDate = pBody.UpdateDate;
|
|
616
|
-
}
|
|
617
|
-
else
|
|
618
|
-
{
|
|
619
|
-
this.fable.log.warn(`No records found in server for max UpdateDate of ${this.EntitySchema.TableName}.`);
|
|
620
|
-
}
|
|
930
|
+
tmpSyncState.Server.RecordCount = pCount;
|
|
931
|
+
this.fable.log.info(`Server count ${this.EntitySchema.TableName}: ${tmpSyncState.Server.RecordCount}`);
|
|
621
932
|
return fStageComplete();
|
|
622
933
|
});
|
|
623
934
|
},
|
|
935
|
+
|
|
936
|
+
// Create a progress tracker so callers (e.g. data-cloner UI) can see Total/Synced
|
|
624
937
|
(fStageComplete) =>
|
|
625
938
|
{
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
939
|
+
this.operation.createProgressTracker(tmpSyncState.Server.RecordCount, `FullSync-${this.EntitySchema.TableName}`);
|
|
940
|
+
return fStageComplete();
|
|
941
|
+
},
|
|
942
|
+
|
|
943
|
+
// ---- Stage 3: UpdateDate-based fast sync ----
|
|
944
|
+
// If we have UpdateDate, compare server record count up to our local
|
|
945
|
+
// max UpdateDate. If it matches local count, existing records are in
|
|
946
|
+
// sync and we only need to pull records newer than that date.
|
|
947
|
+
(fStageComplete) =>
|
|
948
|
+
{
|
|
949
|
+
if (!this._hasUpdateDate || !tmpSyncState.Local.MaxUpdateDate)
|
|
950
|
+
{
|
|
951
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no UpdateDate available; skipping UpdateDate fast-sync.`);
|
|
952
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
953
|
+
return fStageComplete();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const tmpDateStr = this._formatDateForFilter(tmpSyncState.Local.MaxUpdateDate);
|
|
957
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
958
|
+
const tmpBeforeFilter = `FBV~UpdateDate~LE~${tmpDateStr}`;
|
|
959
|
+
|
|
960
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: checking server count with UpdateDate <= ${tmpDateStr}...`);
|
|
961
|
+
|
|
962
|
+
this._getServerCount(tmpBeforeFilter,
|
|
963
|
+
(pError, pServerCountBefore) =>
|
|
629
964
|
{
|
|
630
965
|
if (pError)
|
|
631
966
|
{
|
|
632
|
-
this.fable.log.warn(
|
|
633
|
-
tmpSyncState.
|
|
967
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count before UpdateDate (${pError}); falling back to bisection.`);
|
|
968
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
634
969
|
return fStageComplete();
|
|
635
970
|
}
|
|
636
|
-
|
|
971
|
+
|
|
972
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: server has ${pServerCountBefore} records with UpdateDate <= ${tmpDateStr} (local has ${tmpSyncState.Local.RecordCount})`);
|
|
973
|
+
|
|
974
|
+
if (pServerCountBefore === tmpSyncState.Local.RecordCount)
|
|
637
975
|
{
|
|
638
|
-
|
|
639
|
-
|
|
976
|
+
// Record counts match up to our max UpdateDate -- existing records are in sync.
|
|
977
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts match up to local max UpdateDate; existing records appear in sync.`);
|
|
978
|
+
tmpSyncState.ExistingRecordsInSync = true;
|
|
979
|
+
this._incrementProgress(pServerCountBefore);
|
|
640
980
|
}
|
|
641
981
|
else
|
|
642
982
|
{
|
|
643
|
-
this.fable.log.
|
|
983
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch before max UpdateDate (local: ${tmpSyncState.Local.RecordCount}, server: ${pServerCountBefore}); will bisect existing records.`);
|
|
984
|
+
tmpSyncState.ExistingRecordsInSync = false;
|
|
644
985
|
}
|
|
645
|
-
|
|
986
|
+
|
|
987
|
+
// Now pull records with UpdateDate > local max UpdateDate (new + modified on server)
|
|
988
|
+
const tmpAfterFilter = `FBV~UpdateDate~GT~${tmpDateStr}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
989
|
+
|
|
990
|
+
this._getServerCount(`FBV~UpdateDate~GT~${tmpDateStr}`,
|
|
991
|
+
(pAfterError, pServerCountAfter) =>
|
|
992
|
+
{
|
|
993
|
+
if (pAfterError)
|
|
994
|
+
{
|
|
995
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count after UpdateDate (${pAfterError}).`);
|
|
996
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
997
|
+
return fStageComplete();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ${pServerCountAfter} records on server with UpdateDate > ${tmpDateStr}; pulling...`);
|
|
1001
|
+
|
|
1002
|
+
if (pServerCountAfter < 1)
|
|
1003
|
+
{
|
|
1004
|
+
tmpSyncState.UpdateDateSyncDone = true;
|
|
1005
|
+
// No new records -- nothing additional to count
|
|
1006
|
+
return fStageComplete();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
this._pullServerRecords(tmpAfterFilter, pServerCountAfter,
|
|
1010
|
+
(pPullError, pSyncedCount) =>
|
|
1011
|
+
{
|
|
1012
|
+
if (pPullError)
|
|
1013
|
+
{
|
|
1014
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records: ${pPullError}`);
|
|
1015
|
+
}
|
|
1016
|
+
else
|
|
1017
|
+
{
|
|
1018
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new/modified records via UpdateDate.`);
|
|
1019
|
+
}
|
|
1020
|
+
this._incrementProgress(pSyncedCount || 0);
|
|
1021
|
+
tmpSyncState.UpdateDateSyncDone = true;
|
|
1022
|
+
return fStageComplete();
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
646
1025
|
});
|
|
647
1026
|
},
|
|
1027
|
+
|
|
1028
|
+
// ---- Stage 4: Bisect existing records if counts did not match ----
|
|
648
1029
|
(fStageComplete) =>
|
|
649
1030
|
{
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1031
|
+
// If UpdateDate sync found existing records in sync, or if we have
|
|
1032
|
+
// no local data yet, skip bisection.
|
|
1033
|
+
if (tmpSyncState.ExistingRecordsInSync)
|
|
1034
|
+
{
|
|
1035
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: existing records in sync; skipping bisection.`);
|
|
1036
|
+
return fStageComplete();
|
|
1037
|
+
}
|
|
656
1038
|
|
|
657
|
-
|
|
658
|
-
|
|
1039
|
+
// If we have no local records, there is nothing to bisect
|
|
1040
|
+
if (tmpSyncState.Local.MaxIDEntity < 1)
|
|
1041
|
+
{
|
|
1042
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no local records; skipping bisection.`);
|
|
1043
|
+
return fStageComplete();
|
|
1044
|
+
}
|
|
659
1045
|
|
|
660
|
-
|
|
1046
|
+
// If the UpdateDate fast-sync already ran and pulled new records,
|
|
1047
|
+
// refresh local count to see if we are now in sync
|
|
1048
|
+
if (tmpSyncState.UpdateDateSyncDone)
|
|
1049
|
+
{
|
|
1050
|
+
return this._getLocalCount(0, 0,
|
|
1051
|
+
(pError, pNewLocalCount) =>
|
|
1052
|
+
{
|
|
1053
|
+
if (pError || pNewLocalCount === tmpSyncState.Server.RecordCount)
|
|
1054
|
+
{
|
|
1055
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts now match after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); skipping bisection.`);
|
|
1056
|
+
return fStageComplete();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts still differ after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); bisecting existing records...`);
|
|
1060
|
+
return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// No UpdateDate available -- bisect the full ID range
|
|
1065
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: bisecting full ID range 1-${tmpSyncState.Local.MaxIDEntity}...`);
|
|
1066
|
+
return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
|
|
661
1067
|
},
|
|
1068
|
+
|
|
1069
|
+
// ---- Stage 5: Pull any remaining new records by ID ----
|
|
1070
|
+
// If no UpdateDate sync ran (table lacks UpdateDate), pull records
|
|
1071
|
+
// with ID > local max ID.
|
|
662
1072
|
(fStageComplete) =>
|
|
663
1073
|
{
|
|
664
|
-
if (tmpSyncState.
|
|
1074
|
+
if (tmpSyncState.UpdateDateSyncDone)
|
|
665
1075
|
{
|
|
666
|
-
|
|
1076
|
+
// UpdateDate sync already handled new records
|
|
667
1077
|
return fStageComplete();
|
|
668
1078
|
}
|
|
669
1079
|
|
|
670
|
-
|
|
1080
|
+
if (tmpSyncState.Server.MaxIDEntity <= tmpSyncState.Local.MaxIDEntity)
|
|
1081
|
+
{
|
|
1082
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no new records by ID (server max ${tmpSyncState.Server.MaxIDEntity} <= local max ${tmpSyncState.Local.MaxIDEntity}).`);
|
|
1083
|
+
return fStageComplete();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
1087
|
+
const tmpFilter = `FBV~${tmpIDCol}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
1088
|
+
const tmpEstimated = tmpSyncState.Server.MaxIDEntity - tmpSyncState.Local.MaxIDEntity;
|
|
1089
|
+
|
|
1090
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling new records with ID > ${tmpSyncState.Local.MaxIDEntity} (~${tmpEstimated} estimated)...`);
|
|
671
1091
|
|
|
672
|
-
|
|
1092
|
+
this._pullServerRecords(tmpFilter, tmpEstimated,
|
|
1093
|
+
(pError, pSyncedCount) =>
|
|
1094
|
+
{
|
|
1095
|
+
if (pError)
|
|
1096
|
+
{
|
|
1097
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records by ID: ${pError}`);
|
|
1098
|
+
}
|
|
1099
|
+
else
|
|
1100
|
+
{
|
|
1101
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new records by ID.`);
|
|
1102
|
+
}
|
|
1103
|
+
this._incrementProgress(pSyncedCount || 0);
|
|
1104
|
+
return fStageComplete();
|
|
1105
|
+
});
|
|
673
1106
|
},
|
|
674
1107
|
],
|
|
675
1108
|
(pError) =>
|
|
676
1109
|
{
|
|
677
1110
|
if (pError)
|
|
678
1111
|
{
|
|
679
|
-
this.fable.log.error(`Error performing
|
|
1112
|
+
this.fable.log.error(`Error performing ongoing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
680
1113
|
}
|
|
681
1114
|
|
|
1115
|
+
// Mark progress tracker as complete so the UI shows the correct totals
|
|
1116
|
+
let tmpTracker = this.operation.progressTrackers[`FullSync-${this.EntitySchema.TableName}`];
|
|
1117
|
+
if (tmpTracker)
|
|
1118
|
+
{
|
|
1119
|
+
tmpTracker.CurrentCount = tmpTracker.TotalCount;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ongoing sync complete.`);
|
|
1123
|
+
|
|
682
1124
|
if (this.SyncDeletedRecords)
|
|
683
1125
|
{
|
|
684
1126
|
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
@@ -65,6 +65,18 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
65
65
|
this.MaxRecordsPerEntity = parseInt(this.options.MaxRecordsPerEntity, 10) || 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Tolerance window in milliseconds for cross-database timestamp precision differences.
|
|
69
|
+
// Passed through to Ongoing sync entities for bisection date comparison.
|
|
70
|
+
this.DateTimePrecisionMS = 1000;
|
|
71
|
+
if (this.fable.ProgramConfiguration.hasOwnProperty('DateTimePrecisionMS'))
|
|
72
|
+
{
|
|
73
|
+
this.DateTimePrecisionMS = parseInt(this.fable.ProgramConfiguration.DateTimePrecisionMS, 10) || 1000;
|
|
74
|
+
}
|
|
75
|
+
else if (this.options.hasOwnProperty('DateTimePrecisionMS'))
|
|
76
|
+
{
|
|
77
|
+
this.DateTimePrecisionMS = parseInt(this.options.DateTimePrecisionMS, 10) || 1000;
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
this.MeadowSchema = false;
|
|
69
81
|
this.MeadowSchemaTableList = false;
|
|
70
82
|
|
|
@@ -101,6 +113,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
101
113
|
PageSize: this.options.PageSize || 100,
|
|
102
114
|
SyncDeletedRecords: this.SyncDeletedRecords,
|
|
103
115
|
MaxRecordsPerEntity: this.MaxRecordsPerEntity,
|
|
116
|
+
DateTimePrecisionMS: this.DateTimePrecisionMS,
|
|
104
117
|
};
|
|
105
118
|
|
|
106
119
|
let tmpSyncEntity;
|