Skip to content

Commit ff9ba79

Browse files
author
Oleksandr Dzhychko
committed
fix(vue-model-api): do not evict ReactiveINodeJS from cache when objects manged by it are still used
The effect of this issue was that subscribed Vue elements didn't update when a change happened in Modelix data. Switching to `shallowRef` fixes the issue, because the created `shallowRef`s have a reference to the `ReactiveINodeJS` instances. Implementing and using the `set` functions of `customRef`s would have had the same effect. But the implementation with `shallowRef`s is cleaner, because we also do not have to save the `trigger` functions in `ReactiveINodeJS`. See also https://vuejs.org/guide/extras/reactivity-in-depth#integration-with-external-state-systems Fixes: MODELIX-1041
1 parent fe9a38b commit ff9ba79

File tree

2 files changed

+107
-85
lines changed

2 files changed

+107
-85
lines changed

vue-model-api/src/internal/ReactiveINodeJS.ts

Lines changed: 102 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { IConceptJS, INodeJS } from "@modelix/ts-model-api";
2-
import { customRef, markRaw } from "vue";
2+
import type { Ref } from "vue";
3+
import { markRaw, shallowRef } from "vue";
34
import type { Cache } from "./Cache";
5+
import type { Nullable } from "@modelix/model-client";
46

57
export function toReactiveINodeJS(
68
node: INodeJS,
@@ -37,15 +39,19 @@ function unwrapReactiveINodeJS(
3739
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3840
type INodeReferenceJS = any;
3941

40-
interface TrackAndTrigger {
41-
track: () => void;
42-
trigger: () => void;
43-
}
44-
4542
export class ReactiveINodeJS implements INodeJS {
46-
private byRoleTrackAndTrigger: Map<string | undefined, TrackAndTrigger> =
43+
private byRoleRefToProperty: Map<string, Ref<string | undefined>> = new Map();
44+
private byRoleRefToReferenceTargetNode: Map<
45+
string,
46+
Ref<INodeJS | undefined>
47+
> = new Map();
48+
private byRoleRefToReferenceTargetRef: Map<
49+
string,
50+
Ref<INodeReferenceJS | undefined>
51+
> = new Map();
52+
private byRoleRefToChildren: Map<string | undefined, Ref<INodeJS[]>> =
4753
new Map();
48-
private trackAndTriggerForAllChildren: TrackAndTrigger | undefined;
54+
private refForAllChildren: Ref<INodeJS[]> | undefined = undefined;
4955

5056
constructor(
5157
public readonly unreactiveNode: INodeJS,
@@ -56,6 +62,30 @@ export class ReactiveINodeJS implements INodeJS {
5662
markRaw(this);
5763
}
5864

65+
private propertyGetter = (role: string) =>
66+
this.unreactiveNode.getPropertyValue(role);
67+
68+
private referenceTargetRefGetter = (role: string) =>
69+
this.unreactiveNode.getReferenceTargetRef(role);
70+
71+
private referenceTargetNodeGetter = (role: string) => {
72+
const unreactiveTargetNode =
73+
this.unreactiveNode.getReferenceTargetNode(role);
74+
return unreactiveTargetNode
75+
? toReactiveINodeJS(unreactiveTargetNode, this.cache)
76+
: unreactiveTargetNode;
77+
};
78+
79+
private childrenGetter = (role: string | undefined) =>
80+
this.unreactiveNode
81+
.getChildren(role)
82+
.map((node) => toReactiveINodeJS(node, this.cache));
83+
84+
private allChildrenGetter = () =>
85+
this.unreactiveNode
86+
.getAllChildren()
87+
.map((node) => toReactiveINodeJS(node, this.cache));
88+
5989
getConcept(): IConceptJS | undefined {
6090
return this.unreactiveNode.getConcept();
6191
}
@@ -89,19 +119,19 @@ export class ReactiveINodeJS implements INodeJS {
89119
}
90120

91121
getChildren(role: string | undefined): INodeJS[] {
92-
const { track } = this.getOrCreateTrackAndTriggerForRole(role);
93-
track();
94-
return this.unreactiveNode
95-
.getChildren(role)
96-
.map((node) => toReactiveINodeJS(node, this.cache));
122+
const ref = this.getOrCreateRefForRole(
123+
this.byRoleRefToChildren,
124+
role,
125+
this.childrenGetter,
126+
);
127+
return ref.value;
97128
}
98129

99130
getAllChildren(): INodeJS[] {
100-
const { track } = this.getOrCreateTrackAndTriggerForAllChildren();
101-
track();
102-
return this.unreactiveNode
103-
.getAllChildren()
104-
.map((node) => toReactiveINodeJS(node, this.cache));
131+
if (this.refForAllChildren == undefined) {
132+
this.refForAllChildren = shallowRef(this.allChildrenGetter());
133+
}
134+
return this.refForAllChildren!.value;
105135
}
106136

107137
moveChild(role: string | undefined, index: number, child: INodeJS): void {
@@ -134,18 +164,21 @@ export class ReactiveINodeJS implements INodeJS {
134164
}
135165

136166
getReferenceTargetNode(role: string): INodeJS | undefined {
137-
const { track } = this.getOrCreateTrackAndTriggerForRole(role);
138-
track();
139-
const unreacitveNode = this.unreactiveNode.getReferenceTargetNode(role);
140-
return unreacitveNode
141-
? toReactiveINodeJS(unreacitveNode, this.cache)
142-
: unreacitveNode;
167+
const ref = this.getOrCreateRefForRole(
168+
this.byRoleRefToReferenceTargetNode,
169+
role,
170+
this.referenceTargetNodeGetter,
171+
);
172+
return ref.value;
143173
}
144174

145175
getReferenceTargetRef(role: string): INodeReferenceJS | undefined {
146-
const { track } = this.getOrCreateTrackAndTriggerForRole(role);
147-
track();
148-
return this.unreactiveNode.getReferenceTargetRef(role);
176+
const ref = this.getOrCreateRefForRole(
177+
this.byRoleRefToReferenceTargetRef,
178+
role,
179+
this.referenceTargetRefGetter,
180+
);
181+
return ref.value;
149182
}
150183

151184
setReferenceTargetNode(role: string, target: INodeJS | undefined): void {
@@ -173,69 +206,57 @@ export class ReactiveINodeJS implements INodeJS {
173206
}
174207

175208
getPropertyValue(role: string): string | undefined {
176-
const { track } = this.getOrCreateTrackAndTriggerForRole(role);
177-
track();
178-
return this.unreactiveNode.getPropertyValue(role);
209+
const ref = this.getOrCreateRefForRole(this.byRoleRefToProperty, role, this.propertyGetter);
210+
return ref.value;
179211
}
180212

181213
setPropertyValue(role: string, value: string | undefined): void {
182214
this.unreactiveNode.setPropertyValue(role, value);
183215
}
184216

185-
private getOrCreateTrackAndTriggerForAllChildren(): TrackAndTrigger {
186-
if (this.trackAndTriggerForAllChildren === undefined) {
187-
customRef((track, trigger) => {
188-
this.trackAndTriggerForAllChildren = {
189-
track,
190-
trigger,
191-
};
192-
return {
193-
// Getter and setter ar empty for the same reason as in `getOrCreateTrackAndTriggerForRole`
194-
get() {},
195-
set() {},
196-
};
197-
});
217+
getOrCreateRefForRole<RoleT, ValueT>(
218+
byRoleRefs: Map<RoleT, Ref<ValueT>>,
219+
role: RoleT,
220+
getValue: (role: RoleT) => ValueT,
221+
): Ref<ValueT> {
222+
const maybeCreatedShallowRef = byRoleRefs.get(role);
223+
224+
if (maybeCreatedShallowRef != undefined) {
225+
return maybeCreatedShallowRef;
226+
} else {
227+
const newRef = shallowRef(getValue(role));
228+
byRoleRefs.set(role, newRef);
229+
return newRef;
198230
}
199-
// `this.trackAndTriggerForAllChildren` will always be set, because the `factory`
200-
// argument of `customRef` will be evaluated immedialty.
201-
return this.trackAndTriggerForAllChildren!;
202231
}
203232

204-
private getOrCreateTrackAndTriggerForRole(
205-
role: string | undefined,
206-
): TrackAndTrigger {
207-
const existing = this.byRoleTrackAndTrigger.get(role);
208-
if (existing !== undefined) {
209-
return existing;
233+
triggerChangeInChild(role: Nullable<string>) {
234+
if (this.refForAllChildren) {
235+
this.refForAllChildren.value = this.allChildrenGetter();
236+
}
237+
238+
const normalizedRole = role ?? undefined;
239+
const maybeRef = this.byRoleRefToChildren.get(normalizedRole);
240+
if (maybeRef) {
241+
maybeRef.value = this.childrenGetter(normalizedRole);
242+
}
243+
}
244+
245+
triggerChangeInReference(role: string) {
246+
const maybeTargetNodeRef = this.byRoleRefToReferenceTargetNode.get(role);
247+
if (maybeTargetNodeRef) {
248+
maybeTargetNodeRef.value = this.referenceTargetNodeGetter(role);
249+
}
250+
const maybeTargetRefRef = this.byRoleRefToReferenceTargetRef.get(role);
251+
if (maybeTargetRefRef) {
252+
maybeTargetRefRef.value = this.referenceTargetRefGetter(role);
253+
}
254+
}
255+
256+
triggerChangeInProperty(role: string) {
257+
const maybeRef = this.byRoleRefToProperty.get(role);
258+
if (maybeRef) {
259+
maybeRef.value = this.propertyGetter(role);
210260
}
211-
let created;
212-
customRef((track, trigger) => {
213-
created = {
214-
track,
215-
trigger,
216-
};
217-
this.byRoleTrackAndTrigger.set(role, created);
218-
return {
219-
// The getters and setters will never be called directly
220-
// and therefore the are empty.
221-
// We use `customRef` to get access to a pair of `trigger` and `track`
222-
// to call them directly from outside.
223-
get() {},
224-
set() {},
225-
};
226-
});
227-
// `created` will always be set, because the `factory`
228-
// argument of `customRef` will be evaluated immedialty.
229-
return created!;
230-
}
231-
232-
triggerChangeInRole(role: string | undefined | null) {
233-
const normalizedRole =
234-
role !== undefined && role !== null ? role : undefined;
235-
this.byRoleTrackAndTrigger.get(normalizedRole)?.trigger();
236-
}
237-
238-
triggerChangeInAllChildren() {
239-
this.trackAndTriggerForAllChildren?.trigger();
240261
}
241262
}

vue-model-api/src/internal/handleChange.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ export function handleChange(change: ChangeJS, cache: Cache<ReactiveINodeJS>) {
1313
if (reacitveNode === undefined) {
1414
return;
1515
}
16-
if (change instanceof PropertyChanged || change instanceof ReferenceChanged) {
17-
reacitveNode.triggerChangeInRole(change.role);
16+
if (change instanceof PropertyChanged) {
17+
reacitveNode.triggerChangeInProperty(change.role);
18+
} else if (change instanceof ReferenceChanged) {
19+
reacitveNode.triggerChangeInReference(change.role);
1820
} else if (change instanceof ChildrenChanged) {
19-
reacitveNode.triggerChangeInRole(change.role);
20-
reacitveNode.triggerChangeInAllChildren();
21+
reacitveNode.triggerChangeInChild(change.role);
2122
}
2223
}

0 commit comments

Comments
 (0)