Copilot commented on code in PR #8351:
URL: https://github.com/apache/texera/pull/8351#discussion_r4117011290
##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -797,25 +854,99 @@ 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();
+ }
+
+ /**
+ * 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 without announcing an edit. Used while opening a
workflow, so
- * that merely reading one does not look like a change and trigger a save.
+ * 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
+ * also bounded, so an unreachable y-websocket still ends up with the value
in the document.
+ *
+ * `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 {
+ // Whatever the previously open workflow left waiting is not this
workflow's.
+ this.pendingContentMeta.delete(key);
+ const shared = this.texeraGraph.sharedModel;
+ if (value !== undefined) {
+ this.pendingContentMeta.set(key, value);
+ }
+ const seed = () => {
+ // The workflow may have been closed or swapped while we waited; that
document's seed, and
+ // that workflow's value, are no longer ours to write.
+ if (this.texeraGraph.sharedModel !== shared ||
this.pendingContentMeta.get(key) !== value) {
Review Comment:
An absent database value is not represented in `pendingContentMeta`, so this
guard cannot tell a still-pending `undefined` seed from one superseded by a
user edit. For a workflow without `formBinding`,
`hydrateFormBinding(undefined)`, followed by `setFormBinding(...)` before first
sync, reaches this callback and deletes the user's newly written binding at
line 928; the seeding flag also suppresses notification of that deletion. Track
seed identity/presence even for `undefined` and cancel that seed when a real
local edit occurs.
##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -136,12 +151,53 @@ 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 {
+ // Detach the observer from the previous shared model before attaching to
the new one, so
+ // re-attaching on each opened workflow does not stack listeners on the
same map.
+ this.observedContentMetaMap?.unobserve(this.contentMetaObserver!);
+ // A new document is a new room: nothing in it belongs to the previous
one's co-editors.
+ this.roomOwnedContentMeta.clear();
+ const contentMetaMap = this.texeraGraph.sharedModel.contentMetaMap;
+ this.contentMetaObserver = event => {
+ // A value that arrived from the room belongs to the room: a seed must
not overwrite or
+ // delete it (see seedContentMeta). The first sync delivers a
co-editor's state as a remote
+ // transaction, which is how an opening client learns the room already
holds one.
+ if (!event.transaction.local) {
+ for (const key of event.changes.keys.keys()) {
+ this.roomOwnedContentMeta.add(key as ContentMetaKey);
+ }
+ }
+ if (this.jointGraphWrapper.getReloadingWorkflow()) {
+ return;
+ }
+ if (event.changes.keys.has("formBinding") &&
!this.seedingContentMetaKeys.has("formBinding")) {
Review Comment:
This adds visible collaborative refresh behavior for the Form View/settings
panel, but the PR's testing section provides neither the required side-by-side
before/after screenshot or GIF nor manual interaction steps (click path,
browser, and viewport). Please add that verification evidence for this
interactive frontend flow.
##########
frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts:
##########
@@ -797,25 +854,99 @@ 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();
+ }
+
+ /**
+ * 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 without announcing an edit. Used while opening a
workflow, so
- * that merely reading one does not look like a change and trigger a save.
+ * 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
+ * also bounded, so an unreachable y-websocket still ends up with the value
in the document.
+ *
+ * `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 {
+ // Whatever the previously open workflow left waiting is not this
workflow's.
+ this.pendingContentMeta.delete(key);
+ const shared = this.texeraGraph.sharedModel;
+ if (value !== undefined) {
+ this.pendingContentMeta.set(key, value);
+ }
+ const seed = () => {
+ // The workflow may have been closed or swapped while we waited; that
document's seed, and
+ // that workflow's value, are no longer ours to write.
+ if (this.texeraGraph.sharedModel !== shared ||
this.pendingContentMeta.get(key) !== value) {
+ return;
+ }
+ this.pendingContentMeta.delete(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) {
+ seed();
+ return;
+ }
+ const fallback = setTimeout(seed, CONTENT_META_SEED_TIMEOUT_MS);
Review Comment:
A fixed 5-second fallback can expire on a slow but reachable websocket,
writing the stale database value before the room's initial state arrives. When
synchronization later completes, this write is concurrent with the co-editor's
value and Yjs can select the stale value by client ID—the lost-update scenario
this seed delay is meant to prevent. Keep using the pending database copy for
reads while disconnected, but do not publish it into the shared document until
a successful initial sync (or a definitive offline mode).
--
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]