meadow-integration 1.0.12 → 1.0.13
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,10 @@ 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
|
+
|
|
45
49
|
this.Meadow = false;
|
|
46
50
|
|
|
47
51
|
this.operation = new libMeadowOperation(this.fable);
|
|
@@ -144,6 +148,426 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
144
148
|
return tmpRecordToCommit;
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// ---- REST / Local query helpers ----
|
|
152
|
+
|
|
153
|
+
// Format a date value for use in Meadow REST filter expressions (FBV).
|
|
154
|
+
_formatDateForFilter(pDate)
|
|
155
|
+
{
|
|
156
|
+
return this.fable.Dates.dayJS.utc(pDate).format('YYYY-MM-DDTHH:mm:ss.SSS');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get a count from the remote server, optionally filtered.
|
|
160
|
+
_getServerCount(pFilter, fCallback)
|
|
161
|
+
{
|
|
162
|
+
const tmpURL = pFilter
|
|
163
|
+
? `${this.EntitySchema.TableName}s/Count/FilteredTo/${pFilter}`
|
|
164
|
+
: `${this.EntitySchema.TableName}s/Count`;
|
|
165
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
166
|
+
(pError, pResponse, pBody) =>
|
|
167
|
+
{
|
|
168
|
+
if (pError)
|
|
169
|
+
{
|
|
170
|
+
return fCallback(pError);
|
|
171
|
+
}
|
|
172
|
+
if (pBody && pBody.hasOwnProperty('Count'))
|
|
173
|
+
{
|
|
174
|
+
return fCallback(null, pBody.Count);
|
|
175
|
+
}
|
|
176
|
+
return fCallback(null, 0);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get a page of records from the remote server with a Meadow filter expression.
|
|
181
|
+
_getServerRecords(pFilter, pOffset, pPageSize, fCallback)
|
|
182
|
+
{
|
|
183
|
+
const tmpURL = `${this.EntitySchema.TableName}s/FilteredTo/${pFilter}/${pOffset}/${pPageSize}`;
|
|
184
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
185
|
+
(pError, pResponse, pBody) =>
|
|
186
|
+
{
|
|
187
|
+
if (pError)
|
|
188
|
+
{
|
|
189
|
+
return fCallback(pError);
|
|
190
|
+
}
|
|
191
|
+
if (pBody && Array.isArray(pBody))
|
|
192
|
+
{
|
|
193
|
+
return fCallback(null, pBody);
|
|
194
|
+
}
|
|
195
|
+
return fCallback(null, []);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get a count from the local database with optional ID range filters.
|
|
200
|
+
_getLocalCount(pMinID, pMaxID, fCallback)
|
|
201
|
+
{
|
|
202
|
+
const tmpQuery = this.Meadow.query;
|
|
203
|
+
if (pMinID > 0)
|
|
204
|
+
{
|
|
205
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
206
|
+
}
|
|
207
|
+
if (pMaxID > 0)
|
|
208
|
+
{
|
|
209
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
210
|
+
}
|
|
211
|
+
if (!this._hasDeletedColumn)
|
|
212
|
+
{
|
|
213
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
214
|
+
}
|
|
215
|
+
this.Meadow.doCount(tmpQuery,
|
|
216
|
+
(pError, pQuery, pCount) =>
|
|
217
|
+
{
|
|
218
|
+
if (pError)
|
|
219
|
+
{
|
|
220
|
+
return fCallback(pError);
|
|
221
|
+
}
|
|
222
|
+
return fCallback(null, pCount);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get the max UpdateDate from local records in an ID range.
|
|
227
|
+
_getLocalMaxUpdateDate(pMinID, pMaxID, fCallback)
|
|
228
|
+
{
|
|
229
|
+
const tmpQuery = this.Meadow.query;
|
|
230
|
+
if (pMinID > 0)
|
|
231
|
+
{
|
|
232
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
233
|
+
}
|
|
234
|
+
if (pMaxID > 0)
|
|
235
|
+
{
|
|
236
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
237
|
+
}
|
|
238
|
+
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
239
|
+
tmpQuery.setCap(1);
|
|
240
|
+
if (!this._hasDeletedColumn)
|
|
241
|
+
{
|
|
242
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
243
|
+
}
|
|
244
|
+
this.Meadow.doRead(tmpQuery,
|
|
245
|
+
(pError, pQuery, pRecord) =>
|
|
246
|
+
{
|
|
247
|
+
if (pError)
|
|
248
|
+
{
|
|
249
|
+
return fCallback(pError);
|
|
250
|
+
}
|
|
251
|
+
if (!pRecord || !pRecord.UpdateDate)
|
|
252
|
+
{
|
|
253
|
+
return fCallback(null, false);
|
|
254
|
+
}
|
|
255
|
+
return fCallback(null, pRecord.UpdateDate);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Get the min UpdateDate from local records in an ID range.
|
|
260
|
+
_getLocalMinUpdateDate(pMinID, pMaxID, fCallback)
|
|
261
|
+
{
|
|
262
|
+
const tmpQuery = this.Meadow.query;
|
|
263
|
+
if (pMinID > 0)
|
|
264
|
+
{
|
|
265
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
266
|
+
}
|
|
267
|
+
if (pMaxID > 0)
|
|
268
|
+
{
|
|
269
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
270
|
+
}
|
|
271
|
+
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Ascending' });
|
|
272
|
+
tmpQuery.setCap(1);
|
|
273
|
+
if (!this._hasDeletedColumn)
|
|
274
|
+
{
|
|
275
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
276
|
+
}
|
|
277
|
+
this.Meadow.doRead(tmpQuery,
|
|
278
|
+
(pError, pQuery, pRecord) =>
|
|
279
|
+
{
|
|
280
|
+
if (pError)
|
|
281
|
+
{
|
|
282
|
+
return fCallback(pError);
|
|
283
|
+
}
|
|
284
|
+
if (!pRecord || !pRecord.UpdateDate)
|
|
285
|
+
{
|
|
286
|
+
return fCallback(null, false);
|
|
287
|
+
}
|
|
288
|
+
return fCallback(null, pRecord.UpdateDate);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Upsert a single record from the server into the local database.
|
|
293
|
+
_upsertRecord(pServerRecord, fCallback)
|
|
294
|
+
{
|
|
295
|
+
const tmpRecordToCommit = this.marshalRecord(pServerRecord);
|
|
296
|
+
|
|
297
|
+
const tmpQuery = this.Meadow.query;
|
|
298
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pServerRecord[this.DefaultIdentifier]);
|
|
299
|
+
if (!this._hasDeletedColumn)
|
|
300
|
+
{
|
|
301
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.Meadow.doRead(tmpQuery,
|
|
305
|
+
(pReadError, pQuery, pLocalRecord) =>
|
|
306
|
+
{
|
|
307
|
+
if (pReadError)
|
|
308
|
+
{
|
|
309
|
+
this.fable.log.error(`Error reading local record ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pReadError}`);
|
|
310
|
+
return fCallback();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
314
|
+
tmpSyncQuery.setDisableAutoIdentity(true);
|
|
315
|
+
tmpSyncQuery.setDisableAutoDateStamp(true);
|
|
316
|
+
tmpSyncQuery.setDisableAutoUserStamp(true);
|
|
317
|
+
tmpSyncQuery.setDisableDeleteTracking(true);
|
|
318
|
+
|
|
319
|
+
if (!pLocalRecord)
|
|
320
|
+
{
|
|
321
|
+
// Record does not exist locally -- create
|
|
322
|
+
tmpSyncQuery.AllowIdentityInsert = true;
|
|
323
|
+
this.Meadow.doCreate(tmpSyncQuery,
|
|
324
|
+
(pCreateError) =>
|
|
325
|
+
{
|
|
326
|
+
if (pCreateError)
|
|
327
|
+
{
|
|
328
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
329
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
330
|
+
{
|
|
331
|
+
// GUID conflict -- fall back to update
|
|
332
|
+
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
333
|
+
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
334
|
+
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
335
|
+
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
336
|
+
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
337
|
+
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
338
|
+
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
339
|
+
(pUpdateError) =>
|
|
340
|
+
{
|
|
341
|
+
if (pUpdateError)
|
|
342
|
+
{
|
|
343
|
+
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
344
|
+
}
|
|
345
|
+
return fCallback();
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
350
|
+
return fCallback();
|
|
351
|
+
}
|
|
352
|
+
return fCallback();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else
|
|
356
|
+
{
|
|
357
|
+
// Record exists locally -- update
|
|
358
|
+
this.Meadow.doUpdate(tmpSyncQuery,
|
|
359
|
+
(pUpdateError) =>
|
|
360
|
+
{
|
|
361
|
+
if (pUpdateError)
|
|
362
|
+
{
|
|
363
|
+
this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
|
|
364
|
+
}
|
|
365
|
+
return fCallback();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Pull all records from server matching a filter expression and upsert them locally.
|
|
372
|
+
// Fetches in pages of this.PageSize.
|
|
373
|
+
_pullServerRecords(pFilter, pEstimatedCount, fCallback)
|
|
374
|
+
{
|
|
375
|
+
if (pEstimatedCount < 1)
|
|
376
|
+
{
|
|
377
|
+
return fCallback(null, 0);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let tmpSyncedCount = 0;
|
|
381
|
+
let tmpOffset = 0;
|
|
382
|
+
let tmpDone = false;
|
|
383
|
+
|
|
384
|
+
let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
|
|
385
|
+
? Math.min(pEstimatedCount, this.MaxRecordsPerEntity)
|
|
386
|
+
: pEstimatedCount;
|
|
387
|
+
|
|
388
|
+
const fFetchPage = () =>
|
|
389
|
+
{
|
|
390
|
+
if (tmpDone || tmpOffset >= tmpRecordCap)
|
|
391
|
+
{
|
|
392
|
+
return fCallback(null, tmpSyncedCount);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this._getServerRecords(pFilter, tmpOffset, this.PageSize,
|
|
396
|
+
(pError, pRecords) =>
|
|
397
|
+
{
|
|
398
|
+
if (pError)
|
|
399
|
+
{
|
|
400
|
+
this.fable.log.error(`Error fetching ${this.EntitySchema.TableName} page at offset ${tmpOffset}: ${pError}`);
|
|
401
|
+
return fCallback(pError, tmpSyncedCount);
|
|
402
|
+
}
|
|
403
|
+
if (!pRecords || pRecords.length < 1)
|
|
404
|
+
{
|
|
405
|
+
tmpDone = true;
|
|
406
|
+
return fCallback(null, tmpSyncedCount);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
this.fable.Utility.eachLimit(pRecords, 5,
|
|
410
|
+
(pRecord, fRecordDone) =>
|
|
411
|
+
{
|
|
412
|
+
this._upsertRecord(pRecord,
|
|
413
|
+
() =>
|
|
414
|
+
{
|
|
415
|
+
tmpSyncedCount++;
|
|
416
|
+
return fRecordDone();
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
(pUpsertError) =>
|
|
420
|
+
{
|
|
421
|
+
tmpOffset += this.PageSize;
|
|
422
|
+
if (pRecords.length < this.PageSize)
|
|
423
|
+
{
|
|
424
|
+
tmpDone = true;
|
|
425
|
+
return fCallback(null, tmpSyncedCount);
|
|
426
|
+
}
|
|
427
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${tmpSyncedCount} of ~${tmpRecordCap} records...`);
|
|
428
|
+
return fFetchPage();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
fFetchPage();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---- Bisection logic ----
|
|
437
|
+
|
|
438
|
+
// Compare a local ID range against the server. If counts or date boundaries
|
|
439
|
+
// differ, subdivide until the range is small enough, then pull all records in
|
|
440
|
+
// the range from the server to bring local in sync.
|
|
441
|
+
_bisectRange(pMinID, pMaxID, pDepth, fCallback)
|
|
442
|
+
{
|
|
443
|
+
const tmpRangeSize = pMaxID - pMinID + 1;
|
|
444
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
445
|
+
const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
|
|
446
|
+
|
|
447
|
+
// Get local stats for this range
|
|
448
|
+
this._getLocalCount(pMinID, pMaxID,
|
|
449
|
+
(pLocalCountError, pLocalCount) =>
|
|
450
|
+
{
|
|
451
|
+
if (pLocalCountError)
|
|
452
|
+
{
|
|
453
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect local count error for range ${pMinID}-${pMaxID}: ${pLocalCountError}`);
|
|
454
|
+
return fCallback();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Get server count for this range
|
|
458
|
+
this._getServerCount(tmpRangeFilter,
|
|
459
|
+
(pServerCountError, pServerCount) =>
|
|
460
|
+
{
|
|
461
|
+
if (pServerCountError)
|
|
462
|
+
{
|
|
463
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect server count error for range ${pMinID}-${pMaxID}: ${pServerCountError}`);
|
|
464
|
+
return fCallback();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// If counts match, check UpdateDate boundaries for this range
|
|
468
|
+
if (pLocalCount === pServerCount)
|
|
469
|
+
{
|
|
470
|
+
if (!this._hasUpdateDate)
|
|
471
|
+
{
|
|
472
|
+
// No UpdateDate column -- counts match, assume in sync
|
|
473
|
+
return fCallback();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Compare max and min UpdateDate for this range
|
|
477
|
+
this._getLocalMaxUpdateDate(pMinID, pMaxID,
|
|
478
|
+
(pLocalMaxErr, pLocalMaxDate) =>
|
|
479
|
+
{
|
|
480
|
+
if (pLocalMaxErr || !pLocalMaxDate)
|
|
481
|
+
{
|
|
482
|
+
return fCallback();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Get server max UpdateDate for this range (1 record, sorted desc)
|
|
486
|
+
const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
|
|
487
|
+
this._getServerRecords(tmpMaxDateFilter, 0, 1,
|
|
488
|
+
(pServerMaxErr, pServerMaxRecords) =>
|
|
489
|
+
{
|
|
490
|
+
if (pServerMaxErr || !pServerMaxRecords || pServerMaxRecords.length < 1)
|
|
491
|
+
{
|
|
492
|
+
return fCallback();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const tmpServerMaxDate = pServerMaxRecords[0].UpdateDate;
|
|
496
|
+
const tmpMaxDateDiff = Math.abs(this.fable.Dates.dayJS.utc(pLocalMaxDate).diff(this.fable.Dates.dayJS.utc(tmpServerMaxDate)));
|
|
497
|
+
|
|
498
|
+
if (tmpMaxDateDiff < 5)
|
|
499
|
+
{
|
|
500
|
+
// Max dates match and counts match -- this range is in sync
|
|
501
|
+
return fCallback();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Dates differ even though counts match -- records have been modified.
|
|
505
|
+
// If range is small enough, pull all records; otherwise subdivide.
|
|
506
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: date mismatch in range ${pMinID}-${pMaxID} (local max: ${pLocalMaxDate}, server max: ${tmpServerMaxDate})`);
|
|
507
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
508
|
+
{
|
|
509
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
510
|
+
}
|
|
511
|
+
return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Counts differ
|
|
518
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch in range ${pMinID}-${pMaxID} (local: ${pLocalCount}, server: ${pServerCount})`);
|
|
519
|
+
|
|
520
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
521
|
+
{
|
|
522
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Split an ID range in half and bisect each half.
|
|
531
|
+
_subdivideRange(pMinID, pMaxID, pDepth, fCallback)
|
|
532
|
+
{
|
|
533
|
+
const tmpMidID = Math.floor((pMinID + pMaxID) / 2);
|
|
534
|
+
|
|
535
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: subdividing range ${pMinID}-${pMaxID} at ID ${tmpMidID} (depth ${pDepth})`);
|
|
536
|
+
|
|
537
|
+
// Bisect lower half, then upper half
|
|
538
|
+
this._bisectRange(pMinID, tmpMidID, pDepth + 1,
|
|
539
|
+
() =>
|
|
540
|
+
{
|
|
541
|
+
this._bisectRange(tmpMidID + 1, pMaxID, pDepth + 1, fCallback);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Pull all records from the server in an ID range and upsert them locally.
|
|
546
|
+
_pullRangeFromServer(pMinID, pMaxID, fCallback)
|
|
547
|
+
{
|
|
548
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
549
|
+
const tmpFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
550
|
+
const tmpEstimatedCount = pMaxID - pMinID + 1;
|
|
551
|
+
|
|
552
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling range ${pMinID}-${pMaxID} from server (~${tmpEstimatedCount} records)`);
|
|
553
|
+
|
|
554
|
+
this._pullServerRecords(tmpFilter, tmpEstimatedCount,
|
|
555
|
+
(pError, pSyncedCount) =>
|
|
556
|
+
{
|
|
557
|
+
if (pError)
|
|
558
|
+
{
|
|
559
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling range ${pMinID}-${pMaxID}: ${pError}`);
|
|
560
|
+
}
|
|
561
|
+
else
|
|
562
|
+
{
|
|
563
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: synced ${pSyncedCount} records in range ${pMinID}-${pMaxID}`);
|
|
564
|
+
}
|
|
565
|
+
return fCallback();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---- Deleted records sync ----
|
|
570
|
+
|
|
147
571
|
syncDeletedRecords(fCallback)
|
|
148
572
|
{
|
|
149
573
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
@@ -155,7 +579,6 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
155
579
|
|
|
156
580
|
this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
|
|
157
581
|
|
|
158
|
-
// Get the count of deleted records from the server.
|
|
159
582
|
// The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
|
|
160
583
|
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
|
|
161
584
|
(pError, pResponse, pBody) =>
|
|
@@ -277,149 +700,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
277
700
|
});
|
|
278
701
|
}
|
|
279
702
|
|
|
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
|
-
}
|
|
703
|
+
// ---- Main sync entry point ----
|
|
423
704
|
|
|
424
705
|
sync(fCallback)
|
|
425
706
|
{
|
|
@@ -454,53 +735,44 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
454
735
|
{
|
|
455
736
|
this.operation.createTimeStamp('EntityOngoingSync');
|
|
456
737
|
|
|
457
|
-
let tmpAnticipate = this.fable.newAnticipate();
|
|
458
|
-
|
|
459
738
|
const tmpSyncState = (
|
|
460
739
|
{
|
|
461
|
-
Local: { MaxIDEntity: -1, RecordCount: 0
|
|
462
|
-
Server: { MaxIDEntity: -1, RecordCount: 0
|
|
740
|
+
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
741
|
+
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
463
742
|
});
|
|
464
743
|
|
|
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
|
-
}
|
|
744
|
+
// Detect schema capabilities
|
|
745
|
+
this._hasUpdateDate = false;
|
|
746
|
+
this._hasDeletedColumn = false;
|
|
473
747
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
748
|
+
if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
749
|
+
{
|
|
750
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
751
|
+
{
|
|
752
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
753
|
+
if (tmpColumn.Column == 'UpdateDate')
|
|
754
|
+
{
|
|
755
|
+
this._hasUpdateDate = true;
|
|
756
|
+
}
|
|
757
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
758
|
+
{
|
|
759
|
+
this._hasDeletedColumn = true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
487
763
|
|
|
488
|
-
|
|
489
|
-
{
|
|
490
|
-
this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
|
|
491
|
-
}
|
|
764
|
+
this.fable.log.info(`Syncing with ONGOING STRATEGY entity ${this.EntitySchema.TableName} (UpdateDate: ${this._hasUpdateDate}, Deleted: ${this._hasDeletedColumn})...`);
|
|
492
765
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
766
|
+
this.fable.Utility.waterfall(
|
|
767
|
+
[
|
|
768
|
+
// ---- Stage 1: Gather local stats ----
|
|
496
769
|
(fStageComplete) =>
|
|
497
770
|
{
|
|
498
|
-
//
|
|
771
|
+
// Local max ID
|
|
499
772
|
const tmpQuery = this.Meadow.query;
|
|
500
773
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
501
774
|
tmpQuery.setCap(1);
|
|
502
|
-
|
|
503
|
-
if (!tmpSyncState.HasDeletedColumn)
|
|
775
|
+
if (!this._hasDeletedColumn)
|
|
504
776
|
{
|
|
505
777
|
tmpQuery.setDisableDeleteTracking(true);
|
|
506
778
|
}
|
|
@@ -509,31 +781,32 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
509
781
|
{
|
|
510
782
|
if (pReadError)
|
|
511
783
|
{
|
|
512
|
-
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}
|
|
784
|
+
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
513
785
|
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
514
786
|
}
|
|
515
|
-
if (
|
|
787
|
+
if (pRecord)
|
|
516
788
|
{
|
|
517
|
-
|
|
518
|
-
|
|
789
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
790
|
+
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxIDEntity}`);
|
|
791
|
+
}
|
|
792
|
+
else
|
|
793
|
+
{
|
|
794
|
+
this.fable.log.info(`No local records for ${this.EntitySchema.TableName}.`);
|
|
519
795
|
}
|
|
520
|
-
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
|
|
521
|
-
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
522
796
|
return fStageComplete();
|
|
523
797
|
});
|
|
524
798
|
},
|
|
525
799
|
(fStageComplete) =>
|
|
526
800
|
{
|
|
527
|
-
//
|
|
528
|
-
if (!
|
|
801
|
+
// Local max UpdateDate
|
|
802
|
+
if (!this._hasUpdateDate)
|
|
529
803
|
{
|
|
530
|
-
this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
|
|
531
804
|
return fStageComplete();
|
|
532
805
|
}
|
|
533
806
|
const tmpQuery = this.Meadow.query;
|
|
534
807
|
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
535
808
|
tmpQuery.setCap(1);
|
|
536
|
-
if (!
|
|
809
|
+
if (!this._hasDeletedColumn)
|
|
537
810
|
{
|
|
538
811
|
tmpQuery.setDisableDeleteTracking(true);
|
|
539
812
|
}
|
|
@@ -542,24 +815,22 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
542
815
|
{
|
|
543
816
|
if (pReadError)
|
|
544
817
|
{
|
|
545
|
-
this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}
|
|
818
|
+
this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
546
819
|
return fStageComplete(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
547
820
|
}
|
|
548
|
-
if (
|
|
821
|
+
if (pRecord && pRecord.UpdateDate)
|
|
549
822
|
{
|
|
550
|
-
|
|
551
|
-
|
|
823
|
+
tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
|
|
824
|
+
this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxUpdateDate}`);
|
|
552
825
|
}
|
|
553
|
-
this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${pRecord.UpdateDate}`);
|
|
554
|
-
tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
|
|
555
826
|
return fStageComplete();
|
|
556
827
|
});
|
|
557
828
|
},
|
|
558
829
|
(fStageComplete) =>
|
|
559
830
|
{
|
|
560
|
-
//
|
|
831
|
+
// Local count
|
|
561
832
|
const tmpQuery = this.Meadow.query;
|
|
562
|
-
if (!
|
|
833
|
+
if (!this._hasDeletedColumn)
|
|
563
834
|
{
|
|
564
835
|
tmpQuery.setDisableDeleteTracking(true);
|
|
565
836
|
}
|
|
@@ -568,16 +839,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
568
839
|
{
|
|
569
840
|
if (pCountError)
|
|
570
841
|
{
|
|
571
|
-
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}
|
|
842
|
+
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
572
843
|
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
573
844
|
}
|
|
574
845
|
tmpSyncState.Local.RecordCount = pCount;
|
|
846
|
+
this.fable.log.info(`Local count ${this.EntitySchema.TableName}: ${tmpSyncState.Local.RecordCount}`);
|
|
575
847
|
return fStageComplete();
|
|
576
848
|
});
|
|
577
849
|
},
|
|
850
|
+
|
|
851
|
+
// ---- Stage 2: Gather server stats ----
|
|
578
852
|
(fStageComplete) =>
|
|
579
853
|
{
|
|
580
|
-
//
|
|
854
|
+
// Server max ID
|
|
581
855
|
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
|
|
582
856
|
(pError, pResponse, pBody) =>
|
|
583
857
|
{
|
|
@@ -588,8 +862,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
588
862
|
}
|
|
589
863
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
590
864
|
{
|
|
591
|
-
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
|
|
592
865
|
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
866
|
+
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Server.MaxIDEntity}`);
|
|
593
867
|
}
|
|
594
868
|
else
|
|
595
869
|
{
|
|
@@ -600,85 +874,192 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
600
874
|
},
|
|
601
875
|
(fStageComplete) =>
|
|
602
876
|
{
|
|
603
|
-
//
|
|
604
|
-
this.
|
|
605
|
-
(pError,
|
|
877
|
+
// Server count
|
|
878
|
+
this._getServerCount(null,
|
|
879
|
+
(pError, pCount) =>
|
|
606
880
|
{
|
|
607
881
|
if (pError)
|
|
608
882
|
{
|
|
609
|
-
this.fable.log.warn(`Could not get server
|
|
883
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
884
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
610
885
|
return fStageComplete();
|
|
611
886
|
}
|
|
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
|
-
}
|
|
887
|
+
tmpSyncState.Server.RecordCount = pCount;
|
|
888
|
+
this.fable.log.info(`Server count ${this.EntitySchema.TableName}: ${tmpSyncState.Server.RecordCount}`);
|
|
621
889
|
return fStageComplete();
|
|
622
890
|
});
|
|
623
891
|
},
|
|
892
|
+
|
|
893
|
+
// ---- Stage 3: UpdateDate-based fast sync ----
|
|
894
|
+
// If we have UpdateDate, compare server record count up to our local
|
|
895
|
+
// max UpdateDate. If it matches local count, existing records are in
|
|
896
|
+
// sync and we only need to pull records newer than that date.
|
|
624
897
|
(fStageComplete) =>
|
|
625
898
|
{
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
(
|
|
899
|
+
if (!this._hasUpdateDate || !tmpSyncState.Local.MaxUpdateDate)
|
|
900
|
+
{
|
|
901
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no UpdateDate available; skipping UpdateDate fast-sync.`);
|
|
902
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
903
|
+
return fStageComplete();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const tmpDateStr = this._formatDateForFilter(tmpSyncState.Local.MaxUpdateDate);
|
|
907
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
908
|
+
const tmpBeforeFilter = `FBV~UpdateDate~LE~${tmpDateStr}`;
|
|
909
|
+
|
|
910
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: checking server count with UpdateDate <= ${tmpDateStr}...`);
|
|
911
|
+
|
|
912
|
+
this._getServerCount(tmpBeforeFilter,
|
|
913
|
+
(pError, pServerCountBefore) =>
|
|
629
914
|
{
|
|
630
915
|
if (pError)
|
|
631
916
|
{
|
|
632
|
-
this.fable.log.warn(
|
|
633
|
-
tmpSyncState.
|
|
917
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count before UpdateDate (${pError}); falling back to bisection.`);
|
|
918
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
634
919
|
return fStageComplete();
|
|
635
920
|
}
|
|
636
|
-
|
|
921
|
+
|
|
922
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: server has ${pServerCountBefore} records with UpdateDate <= ${tmpDateStr} (local has ${tmpSyncState.Local.RecordCount})`);
|
|
923
|
+
|
|
924
|
+
if (pServerCountBefore === tmpSyncState.Local.RecordCount)
|
|
637
925
|
{
|
|
638
|
-
|
|
639
|
-
|
|
926
|
+
// Record counts match up to our max UpdateDate -- existing records are in sync.
|
|
927
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts match up to local max UpdateDate; existing records appear in sync.`);
|
|
928
|
+
tmpSyncState.ExistingRecordsInSync = true;
|
|
640
929
|
}
|
|
641
930
|
else
|
|
642
931
|
{
|
|
643
|
-
this.fable.log.
|
|
932
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch before max UpdateDate (local: ${tmpSyncState.Local.RecordCount}, server: ${pServerCountBefore}); will bisect existing records.`);
|
|
933
|
+
tmpSyncState.ExistingRecordsInSync = false;
|
|
644
934
|
}
|
|
645
|
-
|
|
935
|
+
|
|
936
|
+
// Now pull records with UpdateDate > local max UpdateDate (new + modified on server)
|
|
937
|
+
const tmpAfterFilter = `FBV~UpdateDate~GT~${tmpDateStr}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
938
|
+
|
|
939
|
+
this._getServerCount(`FBV~UpdateDate~GT~${tmpDateStr}`,
|
|
940
|
+
(pAfterError, pServerCountAfter) =>
|
|
941
|
+
{
|
|
942
|
+
if (pAfterError)
|
|
943
|
+
{
|
|
944
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count after UpdateDate (${pAfterError}).`);
|
|
945
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
946
|
+
return fStageComplete();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ${pServerCountAfter} records on server with UpdateDate > ${tmpDateStr}; pulling...`);
|
|
950
|
+
|
|
951
|
+
if (pServerCountAfter < 1)
|
|
952
|
+
{
|
|
953
|
+
tmpSyncState.UpdateDateSyncDone = true;
|
|
954
|
+
return fStageComplete();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
this._pullServerRecords(tmpAfterFilter, pServerCountAfter,
|
|
958
|
+
(pPullError, pSyncedCount) =>
|
|
959
|
+
{
|
|
960
|
+
if (pPullError)
|
|
961
|
+
{
|
|
962
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records: ${pPullError}`);
|
|
963
|
+
}
|
|
964
|
+
else
|
|
965
|
+
{
|
|
966
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new/modified records via UpdateDate.`);
|
|
967
|
+
}
|
|
968
|
+
tmpSyncState.UpdateDateSyncDone = true;
|
|
969
|
+
return fStageComplete();
|
|
970
|
+
});
|
|
971
|
+
});
|
|
646
972
|
});
|
|
647
973
|
},
|
|
974
|
+
|
|
975
|
+
// ---- Stage 4: Bisect existing records if counts did not match ----
|
|
648
976
|
(fStageComplete) =>
|
|
649
977
|
{
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
978
|
+
// If UpdateDate sync found existing records in sync, or if we have
|
|
979
|
+
// no local data yet, skip bisection.
|
|
980
|
+
if (tmpSyncState.ExistingRecordsInSync)
|
|
981
|
+
{
|
|
982
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: existing records in sync; skipping bisection.`);
|
|
983
|
+
return fStageComplete();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// If we have no local records, there is nothing to bisect
|
|
987
|
+
if (tmpSyncState.Local.MaxIDEntity < 1)
|
|
988
|
+
{
|
|
989
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no local records; skipping bisection.`);
|
|
990
|
+
return fStageComplete();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// If the UpdateDate fast-sync already ran and pulled new records,
|
|
994
|
+
// refresh local count to see if we are now in sync
|
|
995
|
+
if (tmpSyncState.UpdateDateSyncDone)
|
|
996
|
+
{
|
|
997
|
+
return this._getLocalCount(0, 0,
|
|
998
|
+
(pError, pNewLocalCount) =>
|
|
999
|
+
{
|
|
1000
|
+
if (pError || pNewLocalCount === tmpSyncState.Server.RecordCount)
|
|
1001
|
+
{
|
|
1002
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts now match after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); skipping bisection.`);
|
|
1003
|
+
return fStageComplete();
|
|
1004
|
+
}
|
|
656
1005
|
|
|
657
|
-
|
|
658
|
-
|
|
1006
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts still differ after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); bisecting existing records...`);
|
|
1007
|
+
return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
659
1010
|
|
|
660
|
-
|
|
1011
|
+
// No UpdateDate available -- bisect the full ID range
|
|
1012
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: bisecting full ID range 1-${tmpSyncState.Local.MaxIDEntity}...`);
|
|
1013
|
+
return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
|
|
661
1014
|
},
|
|
1015
|
+
|
|
1016
|
+
// ---- Stage 5: Pull any remaining new records by ID ----
|
|
1017
|
+
// If no UpdateDate sync ran (table lacks UpdateDate), pull records
|
|
1018
|
+
// with ID > local max ID.
|
|
662
1019
|
(fStageComplete) =>
|
|
663
1020
|
{
|
|
664
|
-
if (tmpSyncState.
|
|
1021
|
+
if (tmpSyncState.UpdateDateSyncDone)
|
|
665
1022
|
{
|
|
666
|
-
|
|
1023
|
+
// UpdateDate sync already handled new records
|
|
667
1024
|
return fStageComplete();
|
|
668
1025
|
}
|
|
669
1026
|
|
|
670
|
-
|
|
1027
|
+
if (tmpSyncState.Server.MaxIDEntity <= tmpSyncState.Local.MaxIDEntity)
|
|
1028
|
+
{
|
|
1029
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no new records by ID (server max ${tmpSyncState.Server.MaxIDEntity} <= local max ${tmpSyncState.Local.MaxIDEntity}).`);
|
|
1030
|
+
return fStageComplete();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
1034
|
+
const tmpFilter = `FBV~${tmpIDCol}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
1035
|
+
const tmpEstimated = tmpSyncState.Server.MaxIDEntity - tmpSyncState.Local.MaxIDEntity;
|
|
1036
|
+
|
|
1037
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling new records with ID > ${tmpSyncState.Local.MaxIDEntity} (~${tmpEstimated} estimated)...`);
|
|
671
1038
|
|
|
672
|
-
|
|
1039
|
+
this._pullServerRecords(tmpFilter, tmpEstimated,
|
|
1040
|
+
(pError, pSyncedCount) =>
|
|
1041
|
+
{
|
|
1042
|
+
if (pError)
|
|
1043
|
+
{
|
|
1044
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records by ID: ${pError}`);
|
|
1045
|
+
}
|
|
1046
|
+
else
|
|
1047
|
+
{
|
|
1048
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new records by ID.`);
|
|
1049
|
+
}
|
|
1050
|
+
return fStageComplete();
|
|
1051
|
+
});
|
|
673
1052
|
},
|
|
674
1053
|
],
|
|
675
1054
|
(pError) =>
|
|
676
1055
|
{
|
|
677
1056
|
if (pError)
|
|
678
1057
|
{
|
|
679
|
-
this.fable.log.error(`Error performing
|
|
1058
|
+
this.fable.log.error(`Error performing ongoing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
680
1059
|
}
|
|
681
1060
|
|
|
1061
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ongoing sync complete.`);
|
|
1062
|
+
|
|
682
1063
|
if (this.SyncDeletedRecords)
|
|
683
1064
|
{
|
|
684
1065
|
return this.syncDeletedRecords(() => { return fCallback(); });
|