pxt-core 7.3.22 → 7.3.26

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/built/pxt.js CHANGED
@@ -97885,7 +97885,7 @@ var pxt;
97885
97885
  constructor() {
97886
97886
  this.initialUserPreferences_ = undefined;
97887
97887
  this.initialAuthCheck_ = undefined;
97888
- this.prefPatchOps = [];
97888
+ this.patchQueue = [];
97889
97889
  pxt.Util.assert(!_client);
97890
97890
  // Set global instance.
97891
97891
  _client = this;
@@ -98075,63 +98075,85 @@ var pxt;
98075
98075
  }
98076
98076
  return result.success;
98077
98077
  }
98078
- async patchUserPreferencesAsync(ops, immediate = false) {
98079
- ops = Array.isArray(ops) ? ops : [ops];
98080
- ops = ops.filter(op => !!op);
98078
+ async patchUserPreferencesAsync(patchOps, opts = {}) {
98079
+ const defaultSuccessAsync = async () => ({ success: true, res: await this.userPreferencesAsync() });
98080
+ patchOps = Array.isArray(patchOps) ? patchOps : [patchOps];
98081
+ patchOps = patchOps.filter(op => !!op);
98082
+ if (!patchOps.length) {
98083
+ return await defaultSuccessAsync();
98084
+ }
98085
+ const patchDiff = (pSrc, ops, filter) => {
98086
+ // Apply patches to pDst and return the diff as a set of new patch ops.
98087
+ const pDst = pxt.U.deepCopy(pSrc);
98088
+ ts.pxtc.jsonPatch.patchInPlace(pDst, ops);
98089
+ let diff = ts.pxtc.jsonPatch.diff(pSrc, pDst);
98090
+ // Run caller-provided filter
98091
+ if (diff.length && filter) {
98092
+ diff = diff.filter(filter);
98093
+ }
98094
+ return diff;
98095
+ };
98096
+ // Process incoming patch operations to produce a more fine-grained set of diffs. Incoming patches may be overly destructive
98097
+ // Apply the patch in isolation and get the diff from original
98081
98098
  const curPref = await this.userPreferencesAsync();
98082
- if (!ops.length) {
98083
- return { success: true, res: curPref };
98099
+ const diff = patchDiff(curPref, patchOps, opts.filter);
98100
+ if (!diff.length) {
98101
+ return await defaultSuccessAsync();
98084
98102
  }
98085
- ts.pxtc.jsonPatch.patchInPlace(curPref, ops);
98103
+ // Apply the new diff to the current state
98104
+ ts.pxtc.jsonPatch.patchInPlace(curPref, diff);
98086
98105
  await this.setUserPreferencesAsync(curPref);
98087
- // If we're not logged in, non-persistent local state is all we'll use
98106
+ // If the user is not logged in, non-persistent local state is all we'll use (no sync to cloud)
98088
98107
  if (!await this.loggedInAsync()) {
98089
- return { success: true, res: curPref };
98108
+ return await defaultSuccessAsync();
98090
98109
  }
98091
- // If the user is logged in, save to cloud, but debounce the api call as this can be called frequently from skillmaps
98092
- // Replace matching patches in the queue
98093
- ops.forEach((incoming, iIncoming) => {
98094
- this.prefPatchOps.some((existing, iExisting) => {
98095
- if (!ts.pxtc.jsonPatch.opsAreEqual(existing, incoming))
98096
- return false;
98097
- // Patches are equivalent, replace in queue
98098
- this.prefPatchOps[iExisting] = incoming;
98099
- // Clear from incoming so we don't add it below
98100
- ops[iIncoming] = null;
98101
- return true;
98102
- });
98103
- });
98104
- // Add remaining ops to the queue
98105
- ops.filter(op => !!op).forEach(op => this.prefPatchOps.push(op));
98110
+ // If the user is logged in, sync to cloud, but debounce the api call as this can be called frequently from skillmaps
98111
+ // Queue the patch for sync with backend
98112
+ this.patchQueue.push({ ops: patchOps, filter: opts.filter });
98106
98113
  clearTimeout(debouncePreferencesChangedTimeout);
98107
- const savePrefs = async () => {
98114
+ const syncPrefs = async () => {
98108
98115
  debouncePreferencesChangedStarted = 0;
98109
- // Clear queued patch ops before send.
98110
- const prefPatchOps = this.prefPatchOps;
98111
- this.prefPatchOps = [];
98112
- const result = await this.apiAsync('/api/user/preferences', prefPatchOps, 'PATCH');
98113
- if (result.success) {
98114
- pxt.debug("Updating local user preferences w/ cloud data after result of POST");
98115
- // Set user profile from returned value so we stay in-sync
98116
- this.setUserPreferencesAsync(result.resp);
98116
+ if (!this.patchQueue.length) {
98117
+ return await defaultSuccessAsync();
98118
+ }
98119
+ // Fetch latest prefs from remote
98120
+ const getResult = await this.apiAsync('/api/user/preferences');
98121
+ if (!getResult.success) {
98122
+ pxt.reportError("identity", "failed to fetch preferences for patch", getResult);
98123
+ return { success: false, res: undefined };
98124
+ }
98125
+ // Apply queued patches to the remote state in isolation and develop a final diff to send to the backend
98126
+ const remotePrefs = pxt.U.deepCopy(getResult.resp) || auth.DEFAULT_USER_PREFERENCES();
98127
+ const patchQueue = this.patchQueue;
98128
+ this.patchQueue = []; // Reset the queue
98129
+ patchQueue.forEach(patch => {
98130
+ const diff = patchDiff(remotePrefs, patch.ops, patch.filter);
98131
+ ts.pxtc.jsonPatch.patchInPlace(remotePrefs, diff);
98132
+ });
98133
+ // Diff the original and patched remote states to get a final set of patch operations
98134
+ const finalOps = pxtc.jsonPatch.diff(getResult.resp, remotePrefs);
98135
+ const patchResult = await this.apiAsync('/api/user/preferences', finalOps, 'PATCH');
98136
+ if (patchResult.success) {
98137
+ // Set user profile from returned value so we stay in sync
98138
+ this.setUserPreferencesAsync(patchResult.resp);
98117
98139
  }
98118
98140
  else {
98119
- pxt.reportError("identity", "update preferences failed", result);
98141
+ pxt.reportError("identity", "failed to patch preferences", patchResult);
98120
98142
  }
98121
- return { success: result.success, res: result.resp };
98143
+ return { success: patchResult.success, res: patchResult.resp };
98122
98144
  };
98123
- if (immediate) {
98124
- return await savePrefs();
98145
+ if (opts.immediate) {
98146
+ return await syncPrefs();
98125
98147
  }
98126
98148
  else {
98127
98149
  if (!debouncePreferencesChangedStarted) {
98128
98150
  debouncePreferencesChangedStarted = pxt.U.now();
98129
98151
  }
98130
98152
  if (PREFERENCES_DEBOUNCE_MAX_MS < pxt.U.now() - debouncePreferencesChangedStarted) {
98131
- return await savePrefs();
98153
+ return await syncPrefs();
98132
98154
  }
98133
98155
  else {
98134
- debouncePreferencesChangedTimeout = setTimeout(savePrefs, PREFERENCES_DEBOUNCE_MS);
98156
+ debouncePreferencesChangedTimeout = setTimeout(syncPrefs, PREFERENCES_DEBOUNCE_MS);
98135
98157
  return { success: false, res: undefined }; // This needs to be implemented correctly to return a promise with the debouncer
98136
98158
  }
98137
98159
  }
@@ -99214,6 +99236,18 @@ var ts;
99214
99236
  return r;
99215
99237
  }
99216
99238
  Util.toSet = toSet;
99239
+ function deepCopy(src) {
99240
+ if (typeof src !== "object" || src === null) {
99241
+ return src;
99242
+ }
99243
+ const dst = Array.isArray(src) ? [] : {};
99244
+ for (const key in src) {
99245
+ const value = src[key];
99246
+ dst[key] = deepCopy(value);
99247
+ }
99248
+ return dst;
99249
+ }
99250
+ Util.deepCopy = deepCopy;
99217
99251
  function toArray(a) {
99218
99252
  if (Array.isArray(a)) {
99219
99253
  return a;
package/built/pxtlib.d.ts CHANGED
@@ -129,8 +129,11 @@ declare namespace pxt.auth {
129
129
  username?: string;
130
130
  avatarUrl?: string;
131
131
  }): Promise<boolean>;
132
- private prefPatchOps;
133
- patchUserPreferencesAsync(ops: ts.pxtc.jsonPatch.PatchOperation | ts.pxtc.jsonPatch.PatchOperation[], immediate?: boolean): Promise<SetPrefResult>;
132
+ private patchQueue;
133
+ patchUserPreferencesAsync(patchOps: ts.pxtc.jsonPatch.PatchOperation | ts.pxtc.jsonPatch.PatchOperation[], opts?: {
134
+ immediate?: boolean;
135
+ filter?: (op: ts.pxtc.jsonPatch.PatchOperation) => boolean;
136
+ }): Promise<SetPrefResult>;
134
137
  hasUserId(): boolean;
135
138
  private fetchUserAsync;
136
139
  private setUserProfileAsync;
@@ -302,6 +305,7 @@ declare namespace ts.pxtc.Util {
302
305
  export function groupBy<T>(arr: T[], f: (t: T) => string): pxt.Map<T[]>;
303
306
  export function toDictionary<T>(arr: T[], f: (t: T) => string): pxt.Map<T>;
304
307
  export function toSet<T>(arr: T[], f: (t: T) => string): pxt.Map<boolean>;
308
+ export function deepCopy(src: any): any;
305
309
  export interface ArrayLike<T> {
306
310
  [index: number]: T;
307
311
  length: number;
package/built/pxtlib.js CHANGED
@@ -199,7 +199,7 @@ var pxt;
199
199
  constructor() {
200
200
  this.initialUserPreferences_ = undefined;
201
201
  this.initialAuthCheck_ = undefined;
202
- this.prefPatchOps = [];
202
+ this.patchQueue = [];
203
203
  pxt.Util.assert(!_client);
204
204
  // Set global instance.
205
205
  _client = this;
@@ -389,63 +389,85 @@ var pxt;
389
389
  }
390
390
  return result.success;
391
391
  }
392
- async patchUserPreferencesAsync(ops, immediate = false) {
393
- ops = Array.isArray(ops) ? ops : [ops];
394
- ops = ops.filter(op => !!op);
392
+ async patchUserPreferencesAsync(patchOps, opts = {}) {
393
+ const defaultSuccessAsync = async () => ({ success: true, res: await this.userPreferencesAsync() });
394
+ patchOps = Array.isArray(patchOps) ? patchOps : [patchOps];
395
+ patchOps = patchOps.filter(op => !!op);
396
+ if (!patchOps.length) {
397
+ return await defaultSuccessAsync();
398
+ }
399
+ const patchDiff = (pSrc, ops, filter) => {
400
+ // Apply patches to pDst and return the diff as a set of new patch ops.
401
+ const pDst = pxt.U.deepCopy(pSrc);
402
+ ts.pxtc.jsonPatch.patchInPlace(pDst, ops);
403
+ let diff = ts.pxtc.jsonPatch.diff(pSrc, pDst);
404
+ // Run caller-provided filter
405
+ if (diff.length && filter) {
406
+ diff = diff.filter(filter);
407
+ }
408
+ return diff;
409
+ };
410
+ // Process incoming patch operations to produce a more fine-grained set of diffs. Incoming patches may be overly destructive
411
+ // Apply the patch in isolation and get the diff from original
395
412
  const curPref = await this.userPreferencesAsync();
396
- if (!ops.length) {
397
- return { success: true, res: curPref };
413
+ const diff = patchDiff(curPref, patchOps, opts.filter);
414
+ if (!diff.length) {
415
+ return await defaultSuccessAsync();
398
416
  }
399
- ts.pxtc.jsonPatch.patchInPlace(curPref, ops);
417
+ // Apply the new diff to the current state
418
+ ts.pxtc.jsonPatch.patchInPlace(curPref, diff);
400
419
  await this.setUserPreferencesAsync(curPref);
401
- // If we're not logged in, non-persistent local state is all we'll use
420
+ // If the user is not logged in, non-persistent local state is all we'll use (no sync to cloud)
402
421
  if (!await this.loggedInAsync()) {
403
- return { success: true, res: curPref };
422
+ return await defaultSuccessAsync();
404
423
  }
405
- // If the user is logged in, save to cloud, but debounce the api call as this can be called frequently from skillmaps
406
- // Replace matching patches in the queue
407
- ops.forEach((incoming, iIncoming) => {
408
- this.prefPatchOps.some((existing, iExisting) => {
409
- if (!ts.pxtc.jsonPatch.opsAreEqual(existing, incoming))
410
- return false;
411
- // Patches are equivalent, replace in queue
412
- this.prefPatchOps[iExisting] = incoming;
413
- // Clear from incoming so we don't add it below
414
- ops[iIncoming] = null;
415
- return true;
416
- });
417
- });
418
- // Add remaining ops to the queue
419
- ops.filter(op => !!op).forEach(op => this.prefPatchOps.push(op));
424
+ // If the user is logged in, sync to cloud, but debounce the api call as this can be called frequently from skillmaps
425
+ // Queue the patch for sync with backend
426
+ this.patchQueue.push({ ops: patchOps, filter: opts.filter });
420
427
  clearTimeout(debouncePreferencesChangedTimeout);
421
- const savePrefs = async () => {
428
+ const syncPrefs = async () => {
422
429
  debouncePreferencesChangedStarted = 0;
423
- // Clear queued patch ops before send.
424
- const prefPatchOps = this.prefPatchOps;
425
- this.prefPatchOps = [];
426
- const result = await this.apiAsync('/api/user/preferences', prefPatchOps, 'PATCH');
427
- if (result.success) {
428
- pxt.debug("Updating local user preferences w/ cloud data after result of POST");
429
- // Set user profile from returned value so we stay in-sync
430
- this.setUserPreferencesAsync(result.resp);
430
+ if (!this.patchQueue.length) {
431
+ return await defaultSuccessAsync();
432
+ }
433
+ // Fetch latest prefs from remote
434
+ const getResult = await this.apiAsync('/api/user/preferences');
435
+ if (!getResult.success) {
436
+ pxt.reportError("identity", "failed to fetch preferences for patch", getResult);
437
+ return { success: false, res: undefined };
438
+ }
439
+ // Apply queued patches to the remote state in isolation and develop a final diff to send to the backend
440
+ const remotePrefs = pxt.U.deepCopy(getResult.resp) || auth.DEFAULT_USER_PREFERENCES();
441
+ const patchQueue = this.patchQueue;
442
+ this.patchQueue = []; // Reset the queue
443
+ patchQueue.forEach(patch => {
444
+ const diff = patchDiff(remotePrefs, patch.ops, patch.filter);
445
+ ts.pxtc.jsonPatch.patchInPlace(remotePrefs, diff);
446
+ });
447
+ // Diff the original and patched remote states to get a final set of patch operations
448
+ const finalOps = pxtc.jsonPatch.diff(getResult.resp, remotePrefs);
449
+ const patchResult = await this.apiAsync('/api/user/preferences', finalOps, 'PATCH');
450
+ if (patchResult.success) {
451
+ // Set user profile from returned value so we stay in sync
452
+ this.setUserPreferencesAsync(patchResult.resp);
431
453
  }
432
454
  else {
433
- pxt.reportError("identity", "update preferences failed", result);
455
+ pxt.reportError("identity", "failed to patch preferences", patchResult);
434
456
  }
435
- return { success: result.success, res: result.resp };
457
+ return { success: patchResult.success, res: patchResult.resp };
436
458
  };
437
- if (immediate) {
438
- return await savePrefs();
459
+ if (opts.immediate) {
460
+ return await syncPrefs();
439
461
  }
440
462
  else {
441
463
  if (!debouncePreferencesChangedStarted) {
442
464
  debouncePreferencesChangedStarted = pxt.U.now();
443
465
  }
444
466
  if (PREFERENCES_DEBOUNCE_MAX_MS < pxt.U.now() - debouncePreferencesChangedStarted) {
445
- return await savePrefs();
467
+ return await syncPrefs();
446
468
  }
447
469
  else {
448
- debouncePreferencesChangedTimeout = setTimeout(savePrefs, PREFERENCES_DEBOUNCE_MS);
470
+ debouncePreferencesChangedTimeout = setTimeout(syncPrefs, PREFERENCES_DEBOUNCE_MS);
449
471
  return { success: false, res: undefined }; // This needs to be implemented correctly to return a promise with the debouncer
450
472
  }
451
473
  }
@@ -1528,6 +1550,18 @@ var ts;
1528
1550
  return r;
1529
1551
  }
1530
1552
  Util.toSet = toSet;
1553
+ function deepCopy(src) {
1554
+ if (typeof src !== "object" || src === null) {
1555
+ return src;
1556
+ }
1557
+ const dst = Array.isArray(src) ? [] : {};
1558
+ for (const key in src) {
1559
+ const value = src[key];
1560
+ dst[key] = deepCopy(value);
1561
+ }
1562
+ return dst;
1563
+ }
1564
+ Util.deepCopy = deepCopy;
1531
1565
  function toArray(a) {
1532
1566
  if (Array.isArray(a)) {
1533
1567
  return a;