yangzhang75 commented on code in PR #8351:
URL: https://github.com/apache/texera/pull/8351#discussion_r4117998964


##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -797,25 +861,117 @@ export class WorkflowActionService {
   }
 
   public setWorkflowSettings(workflowSettings: WorkflowSettings | undefined): 
void {
-    if (this.workflowSettings === workflowSettings) {
+    const newSettings = workflowSettings === undefined ? 
this.getDefaultSettings() : workflowSettings;
+    // Skip a redundant write: setting the same value would still cut a Yjs 
update and observer
+    // churn for every collaborator.
+    if (isEqual(this.getWorkflowSettings(), newSettings)) {
       return;
     }
-
-    const newSettings = workflowSettings === undefined ? 
this.getDefaultSettings() : workflowSettings;
-    this.workflowSettings = newSettings;
+    this.texeraGraph.sharedModel.contentMetaMap.set("settings", newSettings);
   }
 
   public getWorkflowSettings(): WorkflowSettings {
-    return this.workflowSettings;
+    return (this.readContentMeta("settings") as WorkflowSettings) ?? 
this.getDefaultSettings();
   }
 
   /**
-   * Load a definition without announcing an edit. Used while opening a 
workflow, so
-   * that merely reading one does not look like a change and trigger a save.
+   * Seed workflowSettings into the shared model while opening a workflow (see 
seedContentMeta).
+   * Called under the reloading flag, so the seed is not announced as an edit.
+   */
+  public hydrateSettings(workflowSettings: WorkflowSettings | undefined): void 
{
+    this.seedContentMeta("settings", workflowSettings);
+  }
+
+  /**
+   * Load a definition into the shared model while opening a workflow (see 
seedContentMeta).
+   * Called under the reloading flag, so the shared-map observer skips this 
seed and opening a
+   * workflow is not announced as an edit.
    */
   public hydrateFormBinding(formBinding: FormBindingConfig | undefined): void {
-    this.formBindingLoaded = formBinding !== undefined;
-    this.formBinding = formBinding ?? getDefaultFormBinding();
+    this.seedContentMeta("formBinding", formBinding);
+  }
+
+  /**
+   * Put the database's copy of a content-meta value into the shared model as 
a workflow opens,
+   * without taking a co-editor's newer one away.
+   *
+   * The document is created and connected just before this runs, so the 
provider has usually not
+   * synced yet. Writing straight away is a concurrent whole-value write, and 
Yjs resolves those
+   * by client id rather than by recency: the copy the database handed us 
could win over an edit a
+   * co-editor has not saved yet -- the lost update this all exists to 
prevent. So wait for the
+   * first sync, then write only when the key is still absent; a value that 
came from the room is
+   * authoritative, whether it is a co-editor's edit or our own from another 
tab.
+   *
+   * Meanwhile the value is not lost: reads fall back to the copy held here, 
so the page shows it
+   * and an autosave that fires before the sync carries it rather than an 
empty one. The wait is
+   * not bounded: a copy published before the sync would be the concurrent 
write above, on a room
+   * that is slow to answer as much as on one that never does. For as long as 
the room stays out
+   * of reach the copy is read from here and every save carries it, and it 
goes into the document
+   * when the sync eventually comes -- or never, which loses nothing, since 
nobody else was in that
+   * document either.
+   *
+   * A local edit of the key while the seed waits cancels it (see 
observeContentMeta): the document
+   * then carries the author's value, which the seed must neither write over 
nor delete.
+   *
+   * `undefined` clears the key, so a workflow that carries no settings or no 
form definition does
+   * not inherit the one the document already held (reloading a version into 
the open document);
+   * a value the room put there is left alone here too.
+   */
+  private seedContentMeta(key: ContentMetaKey, value: ContentMetaValue | 
undefined): void {
+    // This seed replaces whatever the previously open workflow left waiting 
for the key. It has an
+    // identity of its own, since a cleared key (undefined) has no value to be 
told apart by.
+    const seed = {};
+    this.contentMetaSeeds.set(key, seed);
+    this.pendingContentMeta.delete(key);
+    if (value !== undefined) {
+      this.pendingContentMeta.set(key, value);
+    }
+    const shared = this.texeraGraph.sharedModel;
+    const land = () => {
+      // Not ours to write any more: the workflow was closed or swapped while 
we waited (the seed
+      // or the document was replaced), or the author edited the key meanwhile 
(the seed was
+      // cancelled).
+      if (this.texeraGraph.sharedModel !== shared || 
this.contentMetaSeeds.get(key) !== seed) {
+        return;
+      }
+      this.cancelContentMetaSeed(key);
+      // The room's value wins, whether it is a co-editor's edit or this 
client's from another tab.
+      if (this.roomOwnedContentMeta.has(key)) {
+        return;

Review Comment:
   Right: the room's precedence outlived the open and reached every later 
reload into the same document. Fixed in aabcbe763: it applies to the first seed 
of a key on a document only (`seededContentMetaKeys`, reset with the document); 
a later reload's seed -- a version shown or returned from 
(`displayReadonlyWorkflow` / `closeReadonlyWorkflowDisplay`), an agent's 
rewrite -- replaces the value, the room's included, or clears it for a version 
that carries none. Spec: a co-editor's value survives the open; a reload then 
brings another and the document holds that one; a reload without one clears it.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -797,25 +861,117 @@ export class WorkflowActionService {
   }
 
   public setWorkflowSettings(workflowSettings: WorkflowSettings | undefined): 
void {
-    if (this.workflowSettings === workflowSettings) {
+    const newSettings = workflowSettings === undefined ? 
this.getDefaultSettings() : workflowSettings;
+    // Skip a redundant write: setting the same value would still cut a Yjs 
update and observer
+    // churn for every collaborator.
+    if (isEqual(this.getWorkflowSettings(), newSettings)) {
       return;
     }
-
-    const newSettings = workflowSettings === undefined ? 
this.getDefaultSettings() : workflowSettings;
-    this.workflowSettings = newSettings;
+    this.texeraGraph.sharedModel.contentMetaMap.set("settings", newSettings);
   }
 
   public getWorkflowSettings(): WorkflowSettings {
-    return this.workflowSettings;
+    return (this.readContentMeta("settings") as WorkflowSettings) ?? 
this.getDefaultSettings();
   }
 
   /**
-   * Load a definition without announcing an edit. Used while opening a 
workflow, so
-   * that merely reading one does not look like a change and trigger a save.
+   * Seed workflowSettings into the shared model while opening a workflow (see 
seedContentMeta).
+   * Called under the reloading flag, so the seed is not announced as an edit.
+   */
+  public hydrateSettings(workflowSettings: WorkflowSettings | undefined): void 
{
+    this.seedContentMeta("settings", workflowSettings);
+  }
+
+  /**
+   * Load a definition into the shared model while opening a workflow (see 
seedContentMeta).
+   * Called under the reloading flag, so the shared-map observer skips this 
seed and opening a
+   * workflow is not announced as an edit.
    */
   public hydrateFormBinding(formBinding: FormBindingConfig | undefined): void {
-    this.formBindingLoaded = formBinding !== undefined;
-    this.formBinding = formBinding ?? getDefaultFormBinding();
+    this.seedContentMeta("formBinding", formBinding);
+  }
+
+  /**
+   * Put the database's copy of a content-meta value into the shared model as 
a workflow opens,
+   * without taking a co-editor's newer one away.
+   *
+   * The document is created and connected just before this runs, so the 
provider has usually not
+   * synced yet. Writing straight away is a concurrent whole-value write, and 
Yjs resolves those
+   * by client id rather than by recency: the copy the database handed us 
could win over an edit a
+   * co-editor has not saved yet -- the lost update this all exists to 
prevent. So wait for the
+   * first sync, then write only when the key is still absent; a value that 
came from the room is
+   * authoritative, whether it is a co-editor's edit or our own from another 
tab.
+   *
+   * Meanwhile the value is not lost: reads fall back to the copy held here, 
so the page shows it
+   * and an autosave that fires before the sync carries it rather than an 
empty one. The wait is
+   * not bounded: a copy published before the sync would be the concurrent 
write above, on a room
+   * that is slow to answer as much as on one that never does. For as long as 
the room stays out
+   * of reach the copy is read from here and every save carries it, and it 
goes into the document
+   * when the sync eventually comes -- or never, which loses nothing, since 
nobody else was in that
+   * document either.
+   *
+   * A local edit of the key while the seed waits cancels it (see 
observeContentMeta): the document
+   * then carries the author's value, which the seed must neither write over 
nor delete.
+   *
+   * `undefined` clears the key, so a workflow that carries no settings or no 
form definition does
+   * not inherit the one the document already held (reloading a version into 
the open document);
+   * a value the room put there is left alone here too.
+   */
+  private seedContentMeta(key: ContentMetaKey, value: ContentMetaValue | 
undefined): void {
+    // This seed replaces whatever the previously open workflow left waiting 
for the key. It has an
+    // identity of its own, since a cleared key (undefined) has no value to be 
told apart by.
+    const seed = {};
+    this.contentMetaSeeds.set(key, seed);
+    this.pendingContentMeta.delete(key);
+    if (value !== undefined) {
+      this.pendingContentMeta.set(key, value);
+    }
+    const shared = this.texeraGraph.sharedModel;
+    const land = () => {
+      // Not ours to write any more: the workflow was closed or swapped while 
we waited (the seed
+      // or the document was replaced), or the author edited the key meanwhile 
(the seed was
+      // cancelled).
+      if (this.texeraGraph.sharedModel !== shared || 
this.contentMetaSeeds.get(key) !== seed) {
+        return;
+      }
+      this.cancelContentMetaSeed(key);
+      // The room's value wins, whether it is a co-editor's edit or this 
client's from another tab.
+      if (this.roomOwnedContentMeta.has(key)) {
+        return;
+      }
+      // Opening a workflow is not an edit, so the seed is not announced (see 
observeContentMeta).
+      this.seedingContentMetaKeys.add(key);
+      try {
+        if (value === undefined) {
+          shared.contentMetaMap.delete(key);
+        } else if (!shared.contentMetaMap.has(key)) {
+          shared.contentMetaMap.set(key, value);
+        }
+      } finally {
+        this.seedingContentMetaKeys.delete(key);
+      }
+    };
+    if (!shared.wsProvider.shouldConnect || shared.wsProvider.synced) {
+      land();
+      return;
+    }
+    shared.wsProvider.once("sync", land);

Review Comment:
   Done in aabcbe763: each seed owns its listener (`on` with a `detach` on the 
seed, rather than `once`) and drops it when it lands, is superseded or is 
cancelled; loading a new document cancels every seed of the old one. Spec: one 
sync listener per waiting key after two reloads, none after a cancelling edit, 
none once the sync has landed.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -718,7 +774,7 @@ export class WorkflowActionService {
       }
 
       const workflowContent: WorkflowContent = workflow.content;
-      this.workflowSettings = workflowContent.settings || 
this.getDefaultSettings();
+      this.hydrateSettings(workflowContent.settings);

Review Comment:
   One change since the reply above, in aabcbe763: rather than seeding both 
keys in the blank branch, the per-document seed state (waiting copies, seeds, 
room ownership, seeded keys) is reset when a new document is loaded, which is 
what the blank reload does. So the blank branch seeds nothing and 
`clearWorkflow` writes nothing; the same two tests pin it (clearing announces 
nothing and writes nothing; a blank workflow does not read the previous one's 
waiting copy).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to