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


##########
frontend/src/app/workspace/service/workflow-graph/model/shared-model.ts:
##########
@@ -45,6 +45,11 @@ export class SharedModel {
   public operatorLinkMap: Y.Map<OperatorLink>;
   public elementPositionMap: Y.Map<Point>;
   public debugState: Y.Map<Y.Map<BreakpointInfo>>;
+  // Non-graph workflow content that used to live as a per-client private 
field and so was
+  // silently overwritten by another editor's whole-content autosave 
(workflowSettings, and
+  // the Form View definition). Kept in the shared doc, keyed 
"settings"/"formBinding", so it
+  // syncs live like the graph and every autosave writes the current value, 
not a stale copy.
+  public contentMetaMap: Y.Map<unknown>;

Review Comment:
   `contentMetaMap` is typed as `Y.Map<unknown>`, which forces repeated type 
assertions at call-sites and makes it easier to accidentally store unexpected 
values. Consider introducing a small union/type map for known keys (e.g., 
`"settings"` / `"formBinding"`) so callers can get typed values without casting.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -136,12 +131,28 @@ export class WorkflowActionService {
     );
     this.sharedModelChangeHandler.setConfigService(this.config);
     this.workflowMetadata = DEFAULT_WORKFLOW;
-    this.workflowSettings = this.getDefaultSettings();
     
this.undoRedoService.setUndoManager(this.texeraGraph.sharedModel.undoManager);
 
+    // Watch the shared content map, re-attaching whenever the shared model is 
recreated
+    // (opening another workflow), the same way SharedModelChangeHandler 
re-attaches its
+    // graph observers. A formBinding change from a local edit or a co-editor 
is republished
+    // on formBindingChanged$ so the Form View re-renders and the existing 
autosave picks it
+    // up. The reload seed is skipped -- like the graph seed -- so opening a 
workflow is not
+    // announced as an edit and does not save on every open.
+    this.observeContentMeta();
+    this.texeraGraph.newYDocLoadedSubject.subscribe(() => 
this.observeContentMeta());
+
     this.handleJointElementDrag();
   }
 
+  private observeContentMeta(): void {
+    this.texeraGraph.sharedModel.contentMetaMap.observe(event => {
+      if (event.changes.keys.has("formBinding") && 
!this.jointGraphWrapper.getReloadingWorkflow()) {
+        this.formBindingChangeSubject.next(this.getFormBinding());
+      }
+    });
+  }

Review Comment:
   `observeContentMeta()` attaches a new Yjs observer each time it’s called, 
but nothing ever unobserves the previous listener. If `observeContentMeta()` 
can be invoked multiple times for the same `contentMetaMap` instance (or if old 
docs remain referenced), this can cause duplicate `formBindingChanged$` 
emissions and/or memory leaks. Consider storing the observer callback and 
calling `unobserve` before re-attaching, or guard so you only attach once per 
map instance.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -136,12 +131,28 @@ export class WorkflowActionService {
     );
     this.sharedModelChangeHandler.setConfigService(this.config);
     this.workflowMetadata = DEFAULT_WORKFLOW;
-    this.workflowSettings = this.getDefaultSettings();
     
this.undoRedoService.setUndoManager(this.texeraGraph.sharedModel.undoManager);
 
+    // Watch the shared content map, re-attaching whenever the shared model is 
recreated
+    // (opening another workflow), the same way SharedModelChangeHandler 
re-attaches its
+    // graph observers. A formBinding change from a local edit or a co-editor 
is republished
+    // on formBindingChanged$ so the Form View re-renders and the existing 
autosave picks it
+    // up. The reload seed is skipped -- like the graph seed -- so opening a 
workflow is not
+    // announced as an edit and does not save on every open.
+    this.observeContentMeta();
+    this.texeraGraph.newYDocLoadedSubject.subscribe(() => 
this.observeContentMeta());

Review Comment:
   `observeContentMeta()` attaches a new Yjs observer each time it’s called, 
but nothing ever unobserves the previous listener. If `observeContentMeta()` 
can be invoked multiple times for the same `contentMetaMap` instance (or if old 
docs remain referenced), this can cause duplicate `formBindingChanged$` 
emissions and/or memory leaks. Consider storing the observer callback and 
calling `unobserve` before re-attaching, or guard so you only attach once per 
map instance.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -747,25 +758,29 @@ export class WorkflowActionService {
   }
 
   public setWorkflowSettings(workflowSettings: WorkflowSettings | undefined): 
void {
-    if (this.workflowSettings === workflowSettings) {
-      return;
-    }
-
     const newSettings = workflowSettings === undefined ? 
this.getDefaultSettings() : workflowSettings;

Review Comment:
   `setWorkflowSettings` now writes to the shared Yjs map unconditionally. This 
can generate unnecessary Yjs updates/network traffic when callers set the same 
value repeatedly (even when referentially equal). Consider restoring a 
fast-path that compares the current stored value (at least by reference) and 
returns early if unchanged.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.spec.ts:
##########
@@ -891,12 +891,41 @@ describe("WorkflowActionService", () => {
       sub.unsubscribe();
     });
 
-    // Opening a workflow is not an edit; announcing it would save on every 
open.
+    // The definition lives in the shared model (#8315), so a co-editor's 
change -- a write to
+    // the shared map from a remote transaction -- is picked up locally and 
re-rendered, and
+    // this client's next autosave carries the current value instead of a 
stale private copy.
+    it("should pick up a co-editor's change from the shared model", () => {
+      const seen: unknown[] = [];
+      const sub = service.formBindingChanged$.subscribe(v => seen.push(v));
+
+      texeraGraph.sharedModel.contentMetaMap.set("formBinding", config);

Review Comment:
   The test claims it validates a “remote transaction” (co-editor) update, but 
it uses a local `contentMetaMap.set(...)`. Either adjust the comment to reflect 
that this is a local-map observer test, or (if feasible in this test harness) 
simulate a remote update by applying a Yjs update from a separate doc/client to 
better match the scenario described.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -775,16 +790,17 @@ export class WorkflowActionService {
   }
 
   /**
-   * Replace the definition as an edit: announced on `formBindingChanged$`, 
which
-   * feeds workflowChanged() and so reaches the existing autosave.
+   * Replace the definition as an edit. The shared map's observer republishes 
it on
+   * `formBindingChanged$`, which feeds workflowChanged() and so reaches the 
existing autosave.
    */
   public setFormBinding(formBinding: FormBindingConfig): void {

Review Comment:
   Similar to settings, `setFormBinding` always writes to `contentMetaMap`, 
which can create redundant Yjs updates and extra observer churn if invoked with 
the same value. Consider an early return when the stored value is unchanged 
(reference check at minimum).



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -814,11 +830,13 @@ export class WorkflowActionService {
       links,
       commentBoxes,
       settings,
-      // Carry formBinding only when the workflow has one (loaded with it, or 
an author
-      // populated it), so a plain workflow's content is unchanged and its 
save cuts no
-      // needless version.
-      ...(this.formBindingLoaded || 
this.isFormBindingNonEmpty(this.formBinding)
-        ? { formBinding: this.formBinding }
+      // Carry formBinding only when the workflow has one (opened with it, or 
an author
+      // populated it since), so a plain workflow's content is unchanged and 
its save cuts no
+      // needless version. `has` stands in for the old "loaded" flag: hydrate 
sets the key for
+      // a workflow opened with a binding and deletes it for one without.
+      ...(this.texeraGraph.sharedModel.contentMetaMap.has("formBinding") ||
+      this.isFormBindingNonEmpty(this.getFormBinding())
+        ? { formBinding: this.getFormBinding() }
         : {}),

Review Comment:
   `getFormBinding()` is called multiple times while building the returned 
object. To avoid repeated reads and keep the returned snapshot consistent 
(especially if the shared map could change between calls), consider reading it 
once into a local `const formBinding = this.getFormBinding()` and reusing it in 
the condition and payload.



##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.spec.ts:
##########
@@ -891,12 +891,41 @@ describe("WorkflowActionService", () => {
       sub.unsubscribe();
     });
 
-    // Opening a workflow is not an edit; announcing it would save on every 
open.
+    // The definition lives in the shared model (#8315), so a co-editor's 
change -- a write to
+    // the shared map from a remote transaction -- is picked up locally and 
re-rendered, and
+    // this client's next autosave carries the current value instead of a 
stale private copy.
+    it("should pick up a co-editor's change from the shared model", () => {
+      const seen: unknown[] = [];
+      const sub = service.formBindingChanged$.subscribe(v => seen.push(v));
+
+      texeraGraph.sharedModel.contentMetaMap.set("formBinding", config);
+
+      expect(seen).toEqual([config]);
+      expect(service.getFormBinding()).toEqual(config);
+      sub.unsubscribe();
+    });
+
+    // Opening a workflow is not an edit; announcing it would save on every 
open. The seed
+    // runs under the reloading flag, so the shared-map observer skips it.
     it("should stay silent while a workflow is being opened", () => {
       const seen: unknown[] = [];
       const sub = service.formBindingChanged$.subscribe(v => seen.push(v));
 
-      service.hydrateFormBinding(config);
+      service.reloadWorkflow(
+        {
+          ...DEFAULT_WORKFLOW,
+          content: {
+            operators: [mockScanPredicate],
+            operatorPositions: { [mockScanPredicate.operatorID]: mockPoint },
+            links: [],
+            commentBoxes: [],
+            settings: undefined as any,

Review Comment:
   `undefined as any` weakens the test’s type safety and can mask real contract 
changes to `WorkflowContent`. Prefer omitting `settings` if it’s optional, or 
supplying a properly typed `undefined`/default value consistent with the type 
definition (or constructing the object via a typed helper/builder) so the test 
will fail if the API shape changes.



-- 
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