Address reviewer comments

This commit is contained in:
Janet Vorobyeva
2023-09-14 12:37:08 -07:00
parent 3aabd02361
commit 7b193c62a6
6 changed files with 94 additions and 70 deletions

View File

@@ -17,15 +17,20 @@ function nullAsUndefined<T>(value: T|null|undefined): T|undefined {
}
// ================ SequenceNum: used to keep track of cursor edits (lastEditedAt)
// basically just a global auto-incrementing counter, with some types to make intent a bit more clear
// Each cursor starts at SequenceNEVER (0), and after that all cursors using NextSequenceNum() will have
// a unique, monotonically increasing number for their lastEditedAt()
// Basically just a global auto-incrementing counter, with some types to make intent more clear
// Cursors are constructed at SequenceNEVER (0). After that, changes to their sequenceNum will go through
// NextSequenceNum(), so they'll have unique, monotonically increasing numbers for their lastEditedAt()
// NOTE: (by the time the page loads they'll already be at nonzero numbers, the never is intended to be transient)
export type SequenceNum = number;
export const SequenceNEVER: SequenceNum = 0;
export const SequenceNEVER: SequenceNum = 0; // Cursors will start here
let latestGlobalSequenceNum = SequenceNEVER;
const nextSequenceNum = () => { return latestGlobalSequenceNum++; };
function nextSequenceNum() { // First call to this func should return 1
latestGlobalSequenceNum++;
return latestGlobalSequenceNum;
}
// Note: we don't make any provisions for handling overflow. It's fine because:
// NOTE: If latestGlobalSequenceNum overflows, I think it would stop incrementing because of floating point imprecision
// However, we don't need to worry about overflow because:
// - Number.MAX_SAFE_INTEGER is 9,007,199,254,740,991 (9 * 10^15)
// - even at 1000 cursor-edits per second, it would take ~300,000 yrs to overflow
// - Plus it's client-side, so that's a single continuous 300-millenia-long session, which would be impressive uptime
@@ -127,6 +132,10 @@ export class Cursor extends Disposable {
// IMPORTANT: need to subscribe AFTER the properRowId->activeRowId subscription.
// (Cursor-linking observables depend on lastCursorEdit, but only peek at activeRowId. Therefore, updating the
// edit time triggers a re-read of activeRowId, and swapping the order will read stale values for rowId)
// NOTE: this may update sequence number twice for a single edit, but this shouldn't cause any issues.
// For determining priority, this cursor will become the latest edited whether we call it once or twice.
// For updating observables, the double-update might cause cursor-linking observables in LinkingState to
// double-update, but it should be transient and get resolved immediately.
this.autoDispose(this._properRowId.subscribe(() => { this.cursorEdited(); }));
this.autoDispose(this.fieldIndex.subscribe(() => { this.cursorEdited(); }));
@@ -156,26 +165,28 @@ export class Cursor extends Disposable {
* @param isFromLink: should be set if this is a cascading update from cursor-linking
*/
public setCursorPos(cursorPos: CursorPos, isFromLink: boolean = false): void {
//If updating as a result of links, we want to NOT update lastEditedAt
if(isFromLink) { this._silentUpdatesFlag = true; }
//console.log(`CURSOR: ${silentUpdate}, silentUpdate=${this._silentUpdatesFlag},
// lastUpdated = ${this.lastUpdated.peek()}`) //TODO JV DEBUG TEMP
if (cursorPos.rowId !== undefined && this.viewData.getRowIndex(cursorPos.rowId) >= 0) {
this.rowIndex(this.viewData.getRowIndex(cursorPos.rowId) );
} else if (cursorPos.rowIndex !== undefined && cursorPos.rowIndex >= 0) {
this.rowIndex(cursorPos.rowIndex);
} else {
// Write rowIndex to itself to force an update of rowId if needed.
this.rowIndex(this.rowIndex.peek());
}
if (cursorPos.fieldIndex !== undefined) {
this.fieldIndex(cursorPos.fieldIndex);
try {
// If updating as a result of links, we want to NOT update lastEditedAt
if (isFromLink) { this._silentUpdatesFlag = true; }
if (cursorPos.rowId !== undefined && this.viewData.getRowIndex(cursorPos.rowId) >= 0) {
this.rowIndex(this.viewData.getRowIndex(cursorPos.rowId));
} else if (cursorPos.rowIndex !== undefined && cursorPos.rowIndex >= 0) {
this.rowIndex(cursorPos.rowIndex);
} else {
// Write rowIndex to itself to force an update of rowId if needed.
this.rowIndex(this.rowIndex.peek());
}
if (cursorPos.fieldIndex !== undefined) {
this.fieldIndex(cursorPos.fieldIndex);
}
} finally { // Make sure we reset this even on error
this._silentUpdatesFlag = false;
}
//console.log(`CURSOR-END: silentUpdate=${this._silentUpdatesFlag},
// lastEditedAt = ${this._lastEditedAt.peek()} `); //TODO JV DEBUG TEMP
this._silentUpdatesFlag = false;
}
@@ -185,13 +196,13 @@ export class Cursor extends Disposable {
this._isLive(isLive);
}
//Should be called whenever the cursor is updated
//EXCEPT FOR: when cursor is set by linking
//this is used to determine which widget/cursor has most recently been touched,
//and therefore which one should be used to drive linking if there's a conflict
public cursorEdited(): void {
//If updating as a result of links, we want to NOT update lastEdited
if(!this._silentUpdatesFlag)
// Should be called whenever the cursor is updated
// _EXCEPT FOR: when cursor is set by linking
// this is used to determine which widget/cursor has most recently been touched,
// and therefore which one should be used to drive linking if there's a conflict
private cursorEdited(): void {
// If updating as a result of links, we want to NOT update lastEdited
if (!this._silentUpdatesFlag)
{ this._lastEditedAt(nextSequenceNum()); }
}
}

View File

@@ -1135,7 +1135,7 @@ export class GristDoc extends DisposableWithEvents {
}
if (visitedSections.includes(section.id.peek())) {
//We've already been here (we hit a cycle), just return immediately
// We've already been here (we hit a cycle), just return immediately
return true;
}

View File

@@ -1,3 +1,4 @@
import {SequenceNEVER, SequenceNum} from "app/client/components/Cursor";
import {DataRowModel} from "app/client/models/DataRowModel";
import DataTableModel from "app/client/models/DataTableModel";
import {DocModel} from 'app/client/models/DocModel';
@@ -17,7 +18,6 @@ import merge = require('lodash/merge');
import mapValues = require('lodash/mapValues');
import pick = require('lodash/pick');
import pickBy = require('lodash/pickBy');
import {SequenceNEVER, SequenceNum} from "./Cursor";
// Descriptive string enum for each case of linking
@@ -211,7 +211,7 @@ export class LinkingState extends Disposable {
// [ A ]--------/ [ B ] --------------/ [ C ] |
// A.actRowId B.actRowId |
//
// However, if e.g. viewSec B is filtered, the correct rowId might not exist in B, and so it's rowId would be
// However, if e.g. viewSec B is filtered, the correct rowId might not exist in B, and so its activeRowId would be
// on a different row, and therefore the cursor linking would set C to a different row from A, even if it existed
// in C
//
@@ -239,9 +239,10 @@ export class LinkingState extends Disposable {
// viewSection.lastCursorEdit). incomingCursorPos is a pair of [rowId, sequenceNum], so each linkingState sets its
// incomingCursorPos to whichever is most recent between its srcSection, and the previous LS's incCursPos.
//
// The end result is that there is that because the lastCursorEdits are guaranteed to be unique, there is always
// a stable configuration of links, where even in the case of a cycle the incomingCursorPos will all take their
// rowId and version from the most recently edited viewSection in the cycle, which is what the user expects
// If we do this right, the end result is that because the lastCursorEdits are guaranteed to be unique,
// there is always a stable configuration of links, where even in the case of a cycle the incomingCursorPos-es
// will all take their rowId and version from the most recently edited viewSection in the cycle,
// which is what the user expects
//
// ...from C--> [A.LS] --------> [B.LS] --> [C.LS] ----->...to A |
// | | / | |
@@ -252,8 +253,10 @@ export class LinkingState extends Disposable {
// Once the incomingCursorPos-es are determined correctly, the cursorPos-es just need to pull out the rowId,
// and that will drive the cursors of the associated tgt section for each LS.
//
// NOTE: setting cursorPos will change the viewSections' cursor, but it's special-cased to
// not affect their lastCursorEdit times.
// NOTE: setting cursorPos *WILL* change the viewSections' cursor, but it's special-cased to
// so that cursor-driven linking doesn't modify their lastCursorEdit times, so that lastCursorEdit
// reflects only changes driven by external factors
// (e.g. page load, user moving cursor, user changing linking settings/filter settings)
// =============================
// gets the relevant col value for the passed-in rowId, or return rowId unchanged if same-table link
@@ -264,25 +267,31 @@ export class LinkingState extends Disposable {
//Incoming-cursor-pos determines what the linked cursor position should be, considering the previous
//linked section (srcSection) and all upstream sections (through srcSection.linkingState)
this.incomingCursorPos = this.autoDispose((ko.computed(() => {
// Get previous linkingstate's info
// NOTE: prevlink = this.srcSec.linkingState is 2 hops back, (i.e. it reads from from this.srcSec.linkSrcSec)
// The section 1 link back from the current (tgt) section is this.srcSec;
const prevLink = this._srcSection.linkingState?.();
const prevLinkHasCursor = prevLink &&
(prevLink.linkTypeDescription() == "Cursor:Same-Table" ||
prevLink.linkTypeDescription() == "Cursor:Reference");
const [prevLinkedPos, prevLinkedVersion] = prevLinkHasCursor ? prevLink.incomingCursorPos() :
[null, SequenceNEVER];
// Get srcSection's info
// NOTE: This computed primarily decides between srcSec and prevLink. Here's what those mean:
// e.g. consider sections A->B->C, (where this === C)
// We need to decide between taking cursor info from B, our srcSection (1 hop back)
// vs taking cursor info from further back, e.g. A, or before (2+ hops back)
// To take cursor info from further back, we rely on B's linkingState, since B's linkingState will
// be looking at the preceding sections, either A or whatever is behind A.
// Therefore: we either use srcSection (1 back), or prevLink = srcSection.linkingState (2+ back)
// Get srcSection's info (1 hop back)
const srcSecPos = this._srcSection.activeRowId.peek(); //we don't depend on this, only on its cursor version
const srcSecVersion = this._srcSection.lastCursorEdit();
// Sanity check: version might be NEVER if viewSection's cursor hasn't initialized (shouldn't happen I think)
if(srcSecVersion == SequenceNEVER) {
console.warn("=== linkingState: cursor-linking, srcSecVersion = NEVER");
// If cursors haven't been initialized, cursor-linking doesn't make sense, so don't do it
if(srcSecVersion === SequenceNEVER) {
return [null, SequenceNEVER] as [UIRowId|null, SequenceNum];
}
// Get previous linkingstate's info, if applicable (2 or more hops back)
const prevLink = this._srcSection.linkingState?.();
const prevLinkHasCursor = prevLink &&
(prevLink.linkTypeDescription() === "Cursor:Same-Table" ||
prevLink.linkTypeDescription() === "Cursor:Reference");
const [prevLinkedPos, prevLinkedVersion] = prevLinkHasCursor ? prevLink.incomingCursorPos() :
[null, SequenceNEVER];
// ==== Determine whose info to use:
// If prevLinkedVersion < srcSecVersion, then the prev linked data is stale, don't use it
// If prevLinkedVersion == srcSecVersion, then srcSec is the driver for this link cycle (i.e. we're its first