meadow-integration 1.0.29 → 1.0.31

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow-integration",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -688,105 +688,115 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
688
688
 
689
689
  this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
690
690
 
691
- // Generate paginated URLs for deleted records
692
691
  let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
693
692
  ? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
694
693
  : tmpDeletedCount;
695
- const tmpDeleteURLPartials = [];
696
- for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
697
- {
698
- tmpDeleteURLPartials.push(this._appendDeletedQueryString(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`));
699
- }
700
694
 
701
- this.fable.Utility.eachLimit(tmpDeleteURLPartials, 1,
702
- (pURLPartial, fPageComplete) =>
695
+ let tmpOffset = 0;
696
+ let tmpProcessed = 0;
697
+
698
+ // Fetch deleted record pages one at a time rather than
699
+ // pre-generating all URL partials. With millions of deleted
700
+ // records this avoids allocating hundreds of thousands of
701
+ // URL strings and prevents stack overflow in the iterator.
702
+ const fFetchDeletedPage = () =>
703
+ {
704
+ if (tmpOffset >= tmpDeleteCap)
703
705
  {
704
- this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
705
- (pDownloadError, pResponse, pBody) =>
706
+ this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpProcessed} of ${tmpDeletedCount} deleted records processed).`);
707
+ return fCallback();
708
+ }
709
+
710
+ const tmpURL = this._appendDeletedQueryString(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${tmpOffset}/${this.PageSize}`);
711
+ tmpOffset += this.PageSize;
712
+
713
+ this.fable.MeadowCloneRestClient.getJSON(tmpURL,
714
+ (pDownloadError, pResponse, pBody) =>
715
+ {
716
+ if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
706
717
  {
707
- if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
708
- {
709
- return fPageComplete();
710
- }
718
+ this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpProcessed} of ${tmpDeletedCount} deleted records processed).`);
719
+ return fCallback();
720
+ }
711
721
 
712
- this.fable.Utility.eachLimit(pBody, 5,
713
- (pEntityRecord, fRecordComplete) =>
722
+ this.fable.Utility.eachLimit(pBody, 5,
723
+ (pEntityRecord, fRecordComplete) =>
724
+ {
725
+ const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
726
+ if (!tmpRecordID || tmpRecordID < 1)
714
727
  {
715
- const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
716
- if (!tmpRecordID || tmpRecordID < 1)
717
- {
718
- return setImmediate(fRecordComplete);
719
- }
728
+ return setImmediate(fRecordComplete);
729
+ }
720
730
 
721
- // Read local record with delete tracking disabled so we can see all records
722
- const tmpQuery = this.Meadow.query;
723
- tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
724
- tmpQuery.setDisableDeleteTracking(true);
731
+ // Read local record with delete tracking disabled so we can see all records
732
+ const tmpQuery = this.Meadow.query;
733
+ tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
734
+ tmpQuery.setDisableDeleteTracking(true);
725
735
 
726
- this.Meadow.doRead(tmpQuery,
727
- (pReadError, pQuery, pRecord) =>
736
+ this.Meadow.doRead(tmpQuery,
737
+ (pReadError, pQuery, pRecord) =>
738
+ {
739
+ if (pReadError || !pRecord)
728
740
  {
729
- if (pReadError || !pRecord)
730
- {
731
- // Record doesn't exist locally -- create it as deleted
732
- const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
733
-
734
- const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
735
- tmpCreateQuery.setDisableAutoIdentity(true);
736
- tmpCreateQuery.setDisableAutoDateStamp(true);
737
- tmpCreateQuery.setDisableAutoUserStamp(true);
738
- tmpCreateQuery.setDisableDeleteTracking(true);
739
- tmpCreateQuery.AllowIdentityInsert = true;
740
-
741
- this.Meadow.doCreate(tmpCreateQuery,
742
- (pCreateError) =>
743
- {
744
- if (pCreateError)
745
- {
746
- this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
747
- }
748
- return setImmediate(fRecordComplete);
749
- });
750
- return;
751
- }
752
-
753
- if (pRecord.Deleted == 1)
754
- {
755
- // Already marked deleted locally
756
- return setImmediate(fRecordComplete);
757
- }
758
-
759
- // Record exists locally but is not deleted -- update it
741
+ // Record doesn't exist locally -- create it as deleted
760
742
  const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
761
743
 
762
- const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
763
- tmpUpdateQuery.setDisableAutoIdentity(true);
764
- tmpUpdateQuery.setDisableAutoDateStamp(true);
765
- tmpUpdateQuery.setDisableAutoUserStamp(true);
766
- tmpUpdateQuery.setDisableDeleteTracking(true);
744
+ const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
745
+ tmpCreateQuery.setDisableAutoIdentity(true);
746
+ tmpCreateQuery.setDisableAutoDateStamp(true);
747
+ tmpCreateQuery.setDisableAutoUserStamp(true);
748
+ tmpCreateQuery.setDisableDeleteTracking(true);
749
+ tmpCreateQuery.AllowIdentityInsert = true;
767
750
 
768
- this.Meadow.doUpdate(tmpUpdateQuery,
769
- (pUpdateError) =>
751
+ this.Meadow.doCreate(tmpCreateQuery,
752
+ (pCreateError) =>
770
753
  {
771
- if (pUpdateError)
754
+ if (pCreateError)
772
755
  {
773
- this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
756
+ this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
774
757
  }
758
+ tmpProcessed++;
775
759
  return setImmediate(fRecordComplete);
776
760
  });
777
- });
778
- },
779
- (pRecordSyncError) =>
780
- {
781
- return fPageComplete();
782
- });
783
- });
784
- },
785
- (pDeleteSyncError) =>
786
- {
787
- this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpDeletedCount} deleted records processed).`);
788
- return fCallback();
789
- });
761
+ return;
762
+ }
763
+
764
+ if (pRecord.Deleted == 1)
765
+ {
766
+ // Already marked deleted locally
767
+ tmpProcessed++;
768
+ return setImmediate(fRecordComplete);
769
+ }
770
+
771
+ // Record exists locally but is not deleted -- update it
772
+ const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
773
+
774
+ const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
775
+ tmpUpdateQuery.setDisableAutoIdentity(true);
776
+ tmpUpdateQuery.setDisableAutoDateStamp(true);
777
+ tmpUpdateQuery.setDisableAutoUserStamp(true);
778
+ tmpUpdateQuery.setDisableDeleteTracking(true);
779
+
780
+ this.Meadow.doUpdate(tmpUpdateQuery,
781
+ (pUpdateError) =>
782
+ {
783
+ if (pUpdateError)
784
+ {
785
+ this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
786
+ }
787
+ tmpProcessed++;
788
+ return setImmediate(fRecordComplete);
789
+ });
790
+ });
791
+ },
792
+ (pRecordSyncError) =>
793
+ {
794
+ return setImmediate(fFetchDeletedPage);
795
+ });
796
+ });
797
+ };
798
+
799
+ fFetchDeletedPage();
790
800
  });
791
801
  }
792
802
 
@@ -174,119 +174,123 @@ class MeadowSyncEntityOngoingEventualConsistency extends libMeadowSyncEntityOngo
174
174
  let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
175
175
  ? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
176
176
  : tmpDeletedCount;
177
- const tmpDeleteURLPartials = [];
178
- for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
179
- {
180
- tmpDeleteURLPartials.push(this._appendDeletedQueryString(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`));
181
- }
182
177
 
183
178
  const tmpStartTime = Date.now();
184
179
  let tmpProcessed = 0;
185
- let tmpTimeBudgetExhausted = false;
180
+ let tmpOffset = 0;
181
+
182
+ // Fetch deleted record pages one at a time using a recursive
183
+ // fetcher instead of pre-generating all URL partials. With
184
+ // millions of deleted records and a short time budget, the old
185
+ // eachLimit approach generated hundreds of thousands of partials
186
+ // and then had to skip them all when the budget expired.
187
+ const fFetchDeletedPage = () =>
188
+ {
189
+ // Time budget check — stop immediately
190
+ if (Date.now() - tmpStartTime >= this.BackSyncTimeLimit)
191
+ {
192
+ const tmpElapsed = Date.now() - tmpStartTime;
193
+ this.fable.log.info(`Delete sync time budget exhausted for ${this.EntitySchema.TableName} after ${tmpElapsed}ms (${tmpProcessed} of ${tmpDeletedCount} deleted records processed).`);
194
+ return fCallback();
195
+ }
186
196
 
187
- this.fable.Utility.eachLimit(tmpDeleteURLPartials, 1,
188
- (pURLPartial, fPageComplete) =>
197
+ // All pages processed
198
+ if (tmpOffset >= tmpDeleteCap)
189
199
  {
190
- // Check time budget before each page
191
- if (Date.now() - tmpStartTime >= this.BackSyncTimeLimit)
192
- {
193
- tmpTimeBudgetExhausted = true;
194
- return fPageComplete();
195
- }
200
+ const tmpElapsed = Date.now() - tmpStartTime;
201
+ this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpProcessed} of ${tmpDeletedCount} deleted records processed in ${tmpElapsed}ms).`);
202
+ return fCallback();
203
+ }
204
+
205
+ const tmpURL = this._appendDeletedQueryString(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${tmpOffset}/${this.PageSize}`);
206
+ tmpOffset += this.PageSize;
196
207
 
197
- this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
198
- (pDownloadError, pResponse, pBody) =>
208
+ this.fable.MeadowCloneRestClient.getJSON(tmpURL,
209
+ (pDownloadError, pResponse, pBody) =>
210
+ {
211
+ if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
199
212
  {
200
- if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
201
- {
202
- return fPageComplete();
203
- }
213
+ // Empty page or error we've reached the end
214
+ const tmpElapsed = Date.now() - tmpStartTime;
215
+ this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpProcessed} of ${tmpDeletedCount} deleted records processed in ${tmpElapsed}ms).`);
216
+ return fCallback();
217
+ }
204
218
 
205
- this.fable.Utility.eachLimit(pBody, 5,
206
- (pEntityRecord, fRecordComplete) =>
219
+ this.fable.Utility.eachLimit(pBody, 5,
220
+ (pEntityRecord, fRecordComplete) =>
221
+ {
222
+ const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
223
+ if (!tmpRecordID || tmpRecordID < 1)
207
224
  {
208
- const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
209
- if (!tmpRecordID || tmpRecordID < 1)
210
- {
211
- return setImmediate(fRecordComplete);
212
- }
225
+ return setImmediate(fRecordComplete);
226
+ }
213
227
 
214
- const tmpQuery = this.Meadow.query;
215
- tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
216
- tmpQuery.setDisableDeleteTracking(true);
228
+ const tmpQuery = this.Meadow.query;
229
+ tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
230
+ tmpQuery.setDisableDeleteTracking(true);
217
231
 
218
- this.Meadow.doRead(tmpQuery,
219
- (pReadError, pQuery, pRecord) =>
232
+ this.Meadow.doRead(tmpQuery,
233
+ (pReadError, pQuery, pRecord) =>
234
+ {
235
+ if (pReadError || !pRecord)
220
236
  {
221
- if (pReadError || !pRecord)
222
- {
223
- const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
224
-
225
- const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
226
- tmpCreateQuery.setDisableAutoIdentity(true);
227
- tmpCreateQuery.setDisableAutoDateStamp(true);
228
- tmpCreateQuery.setDisableAutoUserStamp(true);
229
- tmpCreateQuery.setDisableDeleteTracking(true);
230
- tmpCreateQuery.AllowIdentityInsert = true;
231
-
232
- this.Meadow.doCreate(tmpCreateQuery,
233
- (pCreateError) =>
234
- {
235
- if (pCreateError)
236
- {
237
- this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
238
- }
239
- tmpProcessed++;
240
- return setImmediate(fRecordComplete);
241
- });
242
- return;
243
- }
244
-
245
- if (pRecord.Deleted == 1)
246
- {
247
- tmpProcessed++;
248
- return setImmediate(fRecordComplete);
249
- }
250
-
251
237
  const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
252
238
 
253
- const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
254
- tmpUpdateQuery.setDisableAutoIdentity(true);
255
- tmpUpdateQuery.setDisableAutoDateStamp(true);
256
- tmpUpdateQuery.setDisableAutoUserStamp(true);
257
- tmpUpdateQuery.setDisableDeleteTracking(true);
239
+ const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
240
+ tmpCreateQuery.setDisableAutoIdentity(true);
241
+ tmpCreateQuery.setDisableAutoDateStamp(true);
242
+ tmpCreateQuery.setDisableAutoUserStamp(true);
243
+ tmpCreateQuery.setDisableDeleteTracking(true);
244
+ tmpCreateQuery.AllowIdentityInsert = true;
258
245
 
259
- this.Meadow.doUpdate(tmpUpdateQuery,
260
- (pUpdateError) =>
246
+ this.Meadow.doCreate(tmpCreateQuery,
247
+ (pCreateError) =>
261
248
  {
262
- if (pUpdateError)
249
+ if (pCreateError)
263
250
  {
264
- this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
251
+ this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
265
252
  }
266
253
  tmpProcessed++;
267
254
  return setImmediate(fRecordComplete);
268
255
  });
269
- });
270
- },
271
- (pRecordSyncError) =>
272
- {
273
- return fPageComplete();
274
- });
275
- });
276
- },
277
- (pDeleteSyncError) =>
278
- {
279
- const tmpElapsed = Date.now() - tmpStartTime;
280
- if (tmpTimeBudgetExhausted)
281
- {
282
- this.fable.log.info(`Delete sync time budget exhausted for ${this.EntitySchema.TableName} after ${tmpElapsed}ms (${tmpProcessed} of ${tmpDeletedCount} deleted records processed).`);
283
- }
284
- else
285
- {
286
- this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpProcessed} of ${tmpDeletedCount} deleted records processed in ${tmpElapsed}ms).`);
287
- }
288
- return fCallback();
289
- });
256
+ return;
257
+ }
258
+
259
+ if (pRecord.Deleted == 1)
260
+ {
261
+ tmpProcessed++;
262
+ return setImmediate(fRecordComplete);
263
+ }
264
+
265
+ const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
266
+
267
+ const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
268
+ tmpUpdateQuery.setDisableAutoIdentity(true);
269
+ tmpUpdateQuery.setDisableAutoDateStamp(true);
270
+ tmpUpdateQuery.setDisableAutoUserStamp(true);
271
+ tmpUpdateQuery.setDisableDeleteTracking(true);
272
+
273
+ this.Meadow.doUpdate(tmpUpdateQuery,
274
+ (pUpdateError) =>
275
+ {
276
+ if (pUpdateError)
277
+ {
278
+ this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
279
+ }
280
+ tmpProcessed++;
281
+ return setImmediate(fRecordComplete);
282
+ });
283
+ });
284
+ },
285
+ (pRecordSyncError) =>
286
+ {
287
+ // Page complete — fetch next page
288
+ return setImmediate(fFetchDeletedPage);
289
+ });
290
+ });
291
+ };
292
+
293
+ fFetchDeletedPage();
290
294
  });
291
295
  }
292
296