Skip to content

Commit 4dbc76e

Browse files
committed
feat(deepReactive): add deepReactive client detection
1 parent fbf29b7 commit 4dbc76e

File tree

4 files changed

+202
-20
lines changed

4 files changed

+202
-20
lines changed

examples/deep_reactive_v2.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from trame.app import TrameApp
2+
from trame.ui.html import DivLayout
3+
from trame.widgets import html
4+
from trame_dataclass.v2 import StateDataModel, Sync, watch
5+
6+
7+
class Data(StateDataModel):
8+
values_reactive = Sync(list[int], list, client_deep_reactive=True)
9+
values = Sync(list[int], list)
10+
11+
@watch("values_reactive")
12+
def _on_values_reactive(self, v):
13+
print("values_reactive", v)
14+
15+
@watch("values")
16+
def _on_values(self, _):
17+
print("values")
18+
19+
20+
class Test(TrameApp):
21+
def __init__(self, server=None):
22+
super().__init__(server)
23+
self.data = Data(self.server)
24+
self.data.values = [1, 2, 3, 4, 5]
25+
self.data.values_reactive = [1, 2, 3, 4, 5]
26+
self._build_ui()
27+
28+
def _build_ui(self):
29+
with DivLayout(self.server) as self.ui:
30+
with self.data.provide_as("data"):
31+
html.Hr()
32+
html.Div("Regular Array")
33+
html.Button("Add", click="data.values.push(1)")
34+
html.Hr()
35+
with html.Div(v_for="v, i in data.values", key="i"):
36+
html.Input(
37+
type="range",
38+
v_model_number="data.values[i]",
39+
min=0,
40+
max=10,
41+
step=1,
42+
)
43+
html.Button("+ (js)", click="data.values[i]++")
44+
html.Button("- (js)", click="data.values[i]--")
45+
html.Hr()
46+
html.Div("Deep reactive Array")
47+
html.Button("Add", click="data.values_reactive.push(1)")
48+
html.Hr()
49+
with html.Div(v_for="v, i in data.values_reactive", key="i"):
50+
html.Input(
51+
type="range",
52+
v_model_number="data.values_reactive[i]",
53+
min=0,
54+
max=10,
55+
step=1,
56+
)
57+
html.Button("+ (js)", click="data.values_reactive[i]++")
58+
html.Button("- (js)", click="data.values_reactive[i]--")
59+
html.Hr()
60+
61+
62+
def main():
63+
app = Test()
64+
app.server.start()
65+
66+
67+
if __name__ == "__main__":
68+
main()

src/trame_dataclass/module/protocol_v2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from loguru import logger
12
from wslink import register as export_rpc
23
from wslink.websocket import LinkProtocol
34

@@ -10,6 +11,7 @@ def compute_definition(trame_dataclass_class):
1011
"name": trame_dataclass_class.__name__,
1112
"dataclass_containers": list(trame_dataclass_class.DATACLASS_NAMES),
1213
"client_only": list(client_only),
14+
"deep_reactive": list(trame_dataclass_class.CLIENT_DEEP_REACTIVE),
1315
}
1416

1517

@@ -77,6 +79,7 @@ def update_state(self, msg):
7779
for dc_id, state in msg.items():
7880
obj = get_instance(dc_id)
7981
if obj is not None:
82+
logger.debug("Update state {}: {}", dc_id, state)
8083
obj.update_from_client_state(state)
8184

8285
def push_delta(self, msg):

src/trame_dataclass/v2.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,13 @@ def _save_field(name, src, dst, encoder=None):
125125

126126
def _setup_class_fields(owner):
127127
# set
128-
for key in ["FIELD_NAMES", "DATACLASS_NAMES", "CLIENT_NAMES", "CLIENT_ONLY_NAMES"]:
128+
for key in [
129+
"FIELD_NAMES",
130+
"DATACLASS_NAMES",
131+
"CLIENT_NAMES",
132+
"CLIENT_ONLY_NAMES",
133+
"CLIENT_DEEP_REACTIVE",
134+
]:
129135
if not hasattr(owner, key):
130136
setattr(owner, key, set())
131137
# dict
@@ -479,8 +485,10 @@ def __init__(
479485
default=None,
480486
convert: FieldEncoder = None,
481487
has_dataclass: bool = False,
488+
client_deep_reactive: bool = False,
482489
type_checking: TypeValidation = TypeValidation.WARNING,
483490
):
491+
self._client_deep_reactive = client_deep_reactive
484492
self._type_checking = type_checking
485493
self._type = get_origin(_type) or _type
486494
if self._type in (Union, types.UnionType):
@@ -547,6 +555,9 @@ class Sync(ServerOnly):
547555
def __set_name__(self, owner, name):
548556
_setup_class_fields(owner)
549557

558+
if self._client_deep_reactive:
559+
owner.CLIENT_DEEP_REACTIVE.add(name)
560+
550561
if self._has_dataclass:
551562
owner.DATACLASS_NAMES.add(name)
552563

@@ -562,6 +573,10 @@ def __set_name__(self, owner, name):
562573
class ClientOnly(ServerOnly):
563574
def __set_name__(self, owner, name):
564575
_setup_class_fields(owner)
576+
577+
if self._client_deep_reactive:
578+
owner.CLIENT_DEEP_REACTIVE.add(name)
579+
565580
self._name = name
566581
owner.TYPE_CHECKING[name] = self._type_checking
567582
owner.FIELD_NAMES.add(name)

vue-components/src/core.js

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ function updateWidget(id, objectState, widgetState) {
1212
Object.assign(widgetState.data, objectState.refs);
1313
}
1414

15+
function updateServerState(serverState, partialState) {
16+
for (const [key, value] of Object.entries(partialState)) {
17+
serverState[key] = JSON.stringify(value);
18+
}
19+
}
20+
1521
export class DataclassManager {
1622
constructor() {
1723
this.client = null;
@@ -24,6 +30,9 @@ export class DataclassManager {
2430
this.dataToVue = {};
2531
this.pendingClientServerQueue = [];
2632
this.pendingFlushRequest = 0;
33+
this.triggers = {};
34+
this.deepReactiveWatchers = {};
35+
this.pendingDeepReactives = {};
2736
}
2837

2938
connect(client) {
@@ -40,10 +49,13 @@ export class DataclassManager {
4049
return;
4150
}
4251

43-
Object.assign(this.dataStates[id].server, state);
52+
// Capture server state for update comparison
53+
updateServerState(this.dataStates[id].server, state);
4454
for (const [key, value] of Object.entries(state)) {
4555
if (this.isDataClass(id, key)) {
4656
await this.handleNestedDataClass(id, key, value);
57+
} else if (this.isDeepReactive(id, key)) {
58+
this.dataStates[id].refs[key].value = reactive(value);
4759
} else {
4860
this.dataStates[id].refs[key].value = value;
4961
}
@@ -52,7 +64,7 @@ export class DataclassManager {
5264
}
5365

5466
updateServer(id, name, value) {
55-
this.pendingClientServerQueue.push([id, name, value]);
67+
this.pendingClientServerQueue.push([id, name, JSON.stringify(value)]);
5668
this.flushToServer();
5769
}
5870

@@ -64,11 +76,12 @@ export class DataclassManager {
6476
const msg = {};
6577
let sendingSomething = 0;
6678
while (this.pendingClientServerQueue.length) {
67-
const [id, name, value] = this.pendingClientServerQueue.shift();
79+
const [id, name, strValue] = this.pendingClientServerQueue.shift();
80+
const value = JSON.parse(strValue);
6881
let valueToSend = value;
6982

7083
// Handle nested dataclass
71-
if (value !== null && this.isDataClass(id, name)) {
84+
if (valueToSend !== null && this.isDataClass(id, name)) {
7285
if (Array.isArray(value)) {
7386
// array[id...]
7487
valueToSend = value.map((v) => v._id);
@@ -86,10 +99,13 @@ export class DataclassManager {
8699
}
87100
}
88101
}
89-
if (
90-
JSON.stringify(this.dataStates[id].server[name]) ===
91-
JSON.stringify(valueToSend)
92-
) {
102+
if (this.dataStates[id].server[name] === strValue) {
103+
console.log(
104+
"skip",
105+
valueToSend,
106+
"server>",
107+
this.dataStates[id].server[name],
108+
);
93109
continue;
94110
}
95111
if (!msg[id]) {
@@ -116,13 +132,45 @@ export class DataclassManager {
116132
}
117133

118134
isDataClass(id, name) {
119-
return this.typeDefinitions[
120-
this.dataTypes[id]
121-
].dataclass_containers.includes(name);
135+
return this.typeDefinitions[this.dataTypes[id]].dataclass_containers[name];
122136
}
123137

124138
isClientOnly(id, name) {
125-
return this.typeDefinitions[this.dataTypes[id]]?.client_only.includes(name);
139+
return this.typeDefinitions[this.dataTypes[id]]?.client_only[name];
140+
}
141+
142+
isDeepReactive(id, name) {
143+
const result =
144+
this.typeDefinitions[this.dataTypes[id]]?.deep_reactive[name];
145+
return result;
146+
}
147+
148+
makeDeepReactive(id, name, value) {
149+
console.log("makeDeepReactive", id, name, value);
150+
const fullKey = `${id}::${name}`;
151+
const unwatch = this.deepReactiveWatchers[fullKey];
152+
if (unwatch) {
153+
console.log("unwatch");
154+
unwatch();
155+
}
156+
this.deepReactiveWatchers[fullKey] = null;
157+
158+
if (value === null || value === undefined) {
159+
return value;
160+
}
161+
162+
const r = reactive(value);
163+
const trigger = this.triggers[fullKey];
164+
if (!trigger) {
165+
if (!this.pendingDeepReactives[id]) {
166+
this.pendingDeepReactives[id] = [];
167+
}
168+
this.pendingDeepReactives[id].push([fullKey, r]);
169+
} else {
170+
console.log("add watch", fullKey);
171+
this.deepReactiveWatchers[fullKey] = watch(r, trigger);
172+
}
173+
return r;
126174
}
127175

128176
async handleNestedDataClass(id, key, value) {
@@ -156,7 +204,19 @@ export class DataclassManager {
156204
}
157205
}
158206
if (!this.dataStates[id].refs[key]) {
159-
this.dataStates[id].refs[key] = ref(newArray);
207+
if (this.isDeepReactive(id, key)) {
208+
this.dataStates[id].refs[key] = ref(
209+
this.makeDeepReactive(id, key, newArray),
210+
);
211+
} else {
212+
this.dataStates[id].refs[key] = ref(newArray);
213+
}
214+
} else if (this.isDeepReactive(id, key)) {
215+
this.dataStates[id].refs[key].value = this.makeDeepReactive(
216+
id,
217+
key,
218+
newArray,
219+
);
160220
} else {
161221
this.dataStates[id].refs[key].value = newArray;
162222
}
@@ -204,6 +264,19 @@ export class DataclassManager {
204264
}
205265
}
206266

267+
getTrigger(id, key, refs) {
268+
const fullKey = `${id}::${key}`;
269+
const fn = this.triggers[fullKey];
270+
if (fn) {
271+
return fn;
272+
}
273+
this.triggers[fullKey] = () => {
274+
this.updateServer(id, key, refs[key].value);
275+
console.log("push to server", id, key);
276+
};
277+
return this.triggers[fullKey];
278+
}
279+
207280
async fetchState(id) {
208281
const refs = { _id: id };
209282
const data = await this.client
@@ -212,7 +285,10 @@ export class DataclassManager {
212285
.call("trame.dataclass.state.get", [id]);
213286

214287
this.dataTypes[id] = data.definition;
215-
this.dataStates[id] = { refs, server: data.state };
288+
this.dataStates[id] = {
289+
refs,
290+
server: JSON.parse(JSON.stringify(data.state)),
291+
};
216292

217293
if (!this.typeDefinitions[data.definition]) {
218294
await this.fetchDefinition(data.definition);
@@ -223,14 +299,22 @@ export class DataclassManager {
223299
if (this.isDataClass(id, key)) {
224300
refs[key] = ref(null);
225301
await this.handleNestedDataClass(id, key, value);
302+
} else if (this.isDeepReactive(id, key)) {
303+
refs[key] = ref(this.makeDeepReactive(id, key, value));
226304
} else {
227305
refs[key] = ref(value);
228306
}
229307
if (!this.isClientOnly(id, key)) {
230-
watch(
231-
() => refs[key].value,
232-
(v) => this.updateServer(id, key, v),
233-
);
308+
const trigger = this.getTrigger(id, key, refs);
309+
watch(refs[key], trigger);
310+
311+
const items = this.pendingDeepReactives[id] || [];
312+
while (items.length) {
313+
const [fullKey, r] = items.pop();
314+
const trigger = this.getTrigger(id, key, refs);
315+
this.deepReactiveWatchers[fullKey] = watch(r, trigger);
316+
console.log("watch", fullKey, r);
317+
}
234318
}
235319
}
236320

@@ -251,7 +335,19 @@ export class DataclassManager {
251335
.getSession()
252336
.call("trame.dataclass.definition.get", [id]);
253337

254-
this.typeDefinitions[id] = data;
338+
this.typeDefinitions[id] = {
339+
...data,
340+
dataclass_containers: {},
341+
client_only: {},
342+
deep_reactive: {},
343+
};
344+
const toDict = ["dataclass_containers", "client_only", "deep_reactive"];
345+
while (toDict.length) {
346+
const arrayName = toDict.pop();
347+
for (let i = 0; i < data[arrayName].length; i++) {
348+
this.typeDefinitions[id][arrayName][data[arrayName][i]] = 1;
349+
}
350+
}
255351
}
256352

257353
unlink(dataId, componentId) {

0 commit comments

Comments
 (0)