Skip to content

Commit 3055977

Browse files
committed
fix(serverState): improve cache with circular dep
1 parent 141ed48 commit 3055977

File tree

2 files changed

+158
-26
lines changed

2 files changed

+158
-26
lines changed

examples/test_client_edits.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import time
2+
3+
from trame.app import TrameApp
4+
from trame.ui.html import DivLayout
5+
from trame.widgets import client, html
6+
from trame_dataclass.v2 import StateDataModel, Sync, watch
7+
8+
9+
class Node(StateDataModel):
10+
name = Sync(str)
11+
prev = Sync(StateDataModel, None, has_dataclass=True)
12+
next = Sync(StateDataModel, None, has_dataclass=True)
13+
14+
15+
class Item(StateDataModel):
16+
value = Sync(int, 1)
17+
name = Sync(str, "hello")
18+
19+
20+
class Container(StateDataModel):
21+
a = Sync(Item, has_dataclass=True)
22+
b = Sync(Item, has_dataclass=True)
23+
items = Sync(list[Item], list, has_dataclass=True)
24+
map = Sync(dict[Item], dict, has_dataclass=True, client_deep_reactive=True)
25+
loop = Sync(Node, has_dataclass=True)
26+
27+
@watch("a")
28+
def _a_change(self, a):
29+
print("a changed", a)
30+
31+
@watch("b")
32+
def _b_change(self, b):
33+
print("b changed", b)
34+
35+
@watch("items")
36+
def _items_change(self, items):
37+
print("items changed", items)
38+
39+
@watch("map")
40+
def _map_change(self, map):
41+
print("map changed", map)
42+
43+
@watch("loop")
44+
def _loop_change(self, loop):
45+
print("loop changed", loop)
46+
47+
48+
class Test(TrameApp):
49+
def __init__(self, server=None):
50+
super().__init__(server)
51+
self.data = Container(self.server)
52+
self.data.a = Item(self.server, value=10)
53+
self.data.b = Item(self.server, value=20)
54+
self.data.items = [Item(self.server, value=i) for i in range(3)]
55+
self.data.map = {f"key_{i}": Item(self.server, value=i) for i in range(3)}
56+
57+
a = Node(self.server, name="a")
58+
b = Node(self.server, name="b")
59+
c = Node(self.server, name="c")
60+
self.data.loop = a
61+
a.next = b
62+
b.next = c
63+
c.next = a
64+
a.prev = c
65+
b.prev = a
66+
c.prev = b
67+
68+
self.hold_refs = [self.data.a, self.data.b, a, b, c]
69+
self._build_ui()
70+
71+
def add(self):
72+
self.data.items = [*self.data.items, Item(self.server, value=time.time())]
73+
74+
def _build_ui(self):
75+
with DivLayout(self.server) as self.ui:
76+
client.Script("""
77+
function swap(data) {
78+
const b = data.b;
79+
const a = data.a;
80+
data.b = a;
81+
data.a = b;
82+
}
83+
""")
84+
with self.data.provide_as("data"):
85+
html.Button("Swap", click="window.swap(data)")
86+
html.Button("Add", click=self.add)
87+
html.Button(
88+
"Pop",
89+
click="data.items = data.items.filter((v,i) => i + 1 < data.items.length)",
90+
)
91+
html.Button(
92+
"Add keys", click="data.map[Date.now()] = data.map['key_0']"
93+
)
94+
html.Button("Next", click="data.loop = data.loop.next")
95+
96+
html.Div("A: {{ data.a }}")
97+
html.Div("B: {{ data.b }}")
98+
99+
with html.Ul():
100+
html.Li("{{ item.value }}", v_for="item, i in data.items", key="i")
101+
102+
with html.Ul():
103+
html.Li("{{ k }}: {{ v }}", v_for="v, k in data.map", key="k")
104+
105+
html.Div("{{ data.loop?.name }}")
106+
html.Div("{{ data.loop?.next?.name }}")
107+
html.Div("{{ data.loop?.next?.next?.name }}")
108+
html.Div("{{ data.loop?.next?.next?.next?.name }}")
109+
html.Div("{{ data.loop?.next?.next?.next?.next?.name }}")
110+
111+
112+
def main():
113+
app = Test()
114+
app.server.start()
115+
116+
117+
if __name__ == "__main__":
118+
main()

vue-components/src/core.js

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ref, reactive, watch } from "vue";
1+
import { ref, reactive, watch, isReactive, toRaw } from "vue";
22

33
function updateWidget(id, objectState, widgetState) {
44
if (widgetState.data._id !== id) {
@@ -14,8 +14,9 @@ function updateWidget(id, objectState, widgetState) {
1414

1515
function updateServerState(serverState, partialState) {
1616
for (const [key, value] of Object.entries(partialState)) {
17-
serverState[key] = JSON.stringify(value);
17+
serverState[key] = JSON.stringify([value]);
1818
}
19+
return serverState;
1920
}
2021

2122
export class DataclassManager {
@@ -68,10 +69,42 @@ export class DataclassManager {
6869
}
6970

7071
updateServer(id, name, value) {
71-
this.pendingClientServerQueue.push([id, name, JSON.stringify(value)]);
72+
if (this.isDataClass(id, name)) {
73+
this.pendingClientServerQueue.push([
74+
id,
75+
name,
76+
JSON.stringify([this.serializeDataClass(value)]),
77+
]);
78+
} else {
79+
this.pendingClientServerQueue.push([id, name, JSON.stringify([value])]);
80+
}
7281
this.flushToServer();
7382
}
7483

84+
serializeDataClass(value) {
85+
if (value === null) {
86+
return value;
87+
}
88+
if (Array.isArray(value)) {
89+
// array[id...]
90+
return value.map((v) => v._id);
91+
}
92+
93+
// dict[str, id] or direct dataclass
94+
if (value._id) {
95+
// => direct dataclass
96+
return value._id;
97+
}
98+
99+
// => dict[str,id]
100+
const result = {};
101+
for (const [k, v] of Object.entries(value)) {
102+
result[k] = v._id;
103+
}
104+
105+
return result;
106+
}
107+
75108
async flushToServer() {
76109
if (this.pendingFlushRequest) {
77110
return;
@@ -81,28 +114,9 @@ export class DataclassManager {
81114
let sendingSomething = 0;
82115
while (this.pendingClientServerQueue.length) {
83116
const [id, name, strValue] = this.pendingClientServerQueue.shift();
84-
const value = JSON.parse(strValue);
117+
const [value] = JSON.parse(strValue);
85118
let valueToSend = value;
86119

87-
// Handle nested dataclass
88-
if (value !== null && this.isDataClass(id, name)) {
89-
if (Array.isArray(value)) {
90-
// array[id...]
91-
valueToSend = value.map((v) => v._id);
92-
} else {
93-
// dict[str, id] or direct dataclass
94-
if (value._id) {
95-
// => direct dataclass
96-
valueToSend = value._id;
97-
} else {
98-
// => dict[str,id]
99-
valueToSend = {};
100-
for (const [k, v] in Object.entries(value)) {
101-
valueToSend[k] = v._id;
102-
}
103-
}
104-
}
105-
}
106120
if (this.dataStates[id].server[name] === strValue) {
107121
continue;
108122
}
@@ -184,7 +198,7 @@ export class DataclassManager {
184198
}
185199
} else if (Array.isArray(value)) {
186200
// array structure
187-
const newArray = [];
201+
const newArray = this.wrapValue(id, key, []);
188202
for (let i = 0; i < value.length; i++) {
189203
const objId = value[i];
190204
let needUpdate = false;
@@ -234,7 +248,7 @@ export class DataclassManager {
234248
}
235249
} else {
236250
// dict structure
237-
const newStruct = reactive({});
251+
const newStruct = this.wrapValue(id, key, {});
238252

239253
for (const [k, objId] of Object.entries(value)) {
240254
if (!this.internalReactiveObjects[objId]) {
@@ -280,7 +294,7 @@ export class DataclassManager {
280294
this.dataTypes[id] = data.definition;
281295
this.dataStates[id] = {
282296
refs,
283-
server: JSON.parse(JSON.stringify(data.state)),
297+
server: updateServerState({}, data.state),
284298
};
285299

286300
if (!this.typeDefinitions[data.definition]) {

0 commit comments

Comments
 (0)