Skip to content

Commit 4eb140b

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

File tree

4 files changed

+193
-19
lines changed

4 files changed

+193
-19
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def compute_definition(trame_dataclass_class):
1010
"name": trame_dataclass_class.__name__,
1111
"dataclass_containers": list(trame_dataclass_class.DATACLASS_NAMES),
1212
"client_only": list(client_only),
13+
"deep_reactive": list(trame_dataclass_class.CLIENT_DEEP_REACTIVE),
1314
}
1415

1516

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: 108 additions & 18 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,7 +76,8 @@ 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
@@ -86,10 +99,7 @@ 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) {
93103
continue;
94104
}
95105
if (!msg[id]) {
@@ -116,13 +126,45 @@ export class DataclassManager {
116126
}
117127

118128
isDataClass(id, name) {
119-
return this.typeDefinitions[
120-
this.dataTypes[id]
121-
].dataclass_containers.includes(name);
129+
return this.typeDefinitions[this.dataTypes[id]].dataclass_containers[name];
122130
}
123131

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

128170
async handleNestedDataClass(id, key, value) {
@@ -156,7 +198,19 @@ export class DataclassManager {
156198
}
157199
}
158200
if (!this.dataStates[id].refs[key]) {
159-
this.dataStates[id].refs[key] = ref(newArray);
201+
if (this.isDeepReactive(id, key)) {
202+
this.dataStates[id].refs[key] = ref(
203+
this.makeDeepReactive(id, key, newArray),
204+
);
205+
} else {
206+
this.dataStates[id].refs[key] = ref(newArray);
207+
}
208+
} else if (this.isDeepReactive(id, key)) {
209+
this.dataStates[id].refs[key].value = this.makeDeepReactive(
210+
id,
211+
key,
212+
newArray,
213+
);
160214
} else {
161215
this.dataStates[id].refs[key].value = newArray;
162216
}
@@ -204,6 +258,19 @@ export class DataclassManager {
204258
}
205259
}
206260

261+
getTrigger(id, key, refs) {
262+
const fullKey = `${id}::${key}`;
263+
const fn = this.triggers[fullKey];
264+
if (fn) {
265+
return fn;
266+
}
267+
this.triggers[fullKey] = () => {
268+
this.updateServer(id, key, refs[key].value);
269+
console.log("push to server", id, key);
270+
};
271+
return this.triggers[fullKey];
272+
}
273+
207274
async fetchState(id) {
208275
const refs = { _id: id };
209276
const data = await this.client
@@ -212,7 +279,10 @@ export class DataclassManager {
212279
.call("trame.dataclass.state.get", [id]);
213280

214281
this.dataTypes[id] = data.definition;
215-
this.dataStates[id] = { refs, server: data.state };
282+
this.dataStates[id] = {
283+
refs,
284+
server: JSON.parse(JSON.stringify(data.state)),
285+
};
216286

217287
if (!this.typeDefinitions[data.definition]) {
218288
await this.fetchDefinition(data.definition);
@@ -223,14 +293,22 @@ export class DataclassManager {
223293
if (this.isDataClass(id, key)) {
224294
refs[key] = ref(null);
225295
await this.handleNestedDataClass(id, key, value);
296+
} else if (this.isDeepReactive(id, key)) {
297+
refs[key] = ref(this.makeDeepReactive(id, key, value));
226298
} else {
227299
refs[key] = ref(value);
228300
}
229301
if (!this.isClientOnly(id, key)) {
230-
watch(
231-
() => refs[key].value,
232-
(v) => this.updateServer(id, key, v),
233-
);
302+
const trigger = this.getTrigger(id, key, refs);
303+
watch(refs[key], trigger);
304+
305+
const items = this.pendingDeepReactives[id] || [];
306+
while (items.length) {
307+
const [fullKey, r] = items.pop();
308+
const trigger = this.getTrigger(id, key, refs);
309+
this.deepReactiveWatchers[fullKey] = watch(r, trigger);
310+
console.log("watch", fullKey, r);
311+
}
234312
}
235313
}
236314

@@ -251,7 +329,19 @@ export class DataclassManager {
251329
.getSession()
252330
.call("trame.dataclass.definition.get", [id]);
253331

254-
this.typeDefinitions[id] = data;
332+
this.typeDefinitions[id] = {
333+
...data,
334+
dataclass_containers: {},
335+
client_only: {},
336+
deep_reactive: {},
337+
};
338+
const toDict = ["dataclass_containers", "client_only", "deep_reactive"];
339+
while (toDict.length) {
340+
const arrayName = toDict.pop();
341+
for (let i = 0; i < data[arrayName].length; i++) {
342+
this.typeDefinitions[id][arrayName][data[arrayName][i]] = 1;
343+
}
344+
}
255345
}
256346

257347
unlink(dataId, componentId) {

0 commit comments

Comments
 (0)