@reactpy/client 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +282 -14
- package/dist/components.js.map +1 -1
- package/dist/handler.d.ts +19 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +19 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +2 -2
- package/dist/mount.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vdom.d.ts +1 -1
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +35 -3
- package/dist/vdom.js.map +1 -1
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +13 -0
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
- package/src/components.tsx +317 -14
- package/src/handler.ts +33 -0
- package/src/index.ts +1 -0
- package/src/mount.tsx +2 -5
- package/src/types.ts +3 -1
- package/src/vdom.tsx +62 -12
- package/src/websocket.ts +14 -0
package/dist/vdom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vdom.js","sourceRoot":"","sources":["../src/vdom.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"vdom.js","sourceRoot":"","sources":["../src/vdom.tsx"],"names":[],"mappings":"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"}
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,SAAS,CAAC;AAgBhE,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gCAAgC;cAOb,SAAS;EAmDpC;AAED,wBAAgB,YAAY,CAC1B,eAAe,EAAE,MAAM,EACvB,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,MAAM,GAClB,MAAM,CAOR"}
|
package/dist/websocket.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import log from "./logger";
|
|
2
|
+
function syncBrowserLocation(url) {
|
|
3
|
+
// The window will always have a HTTP path, so ReactPy should always be aware of it.
|
|
4
|
+
url.searchParams.set("path", window.location.pathname);
|
|
5
|
+
if (window.location.search) {
|
|
6
|
+
// Set the query string parameter if the HTTP location has a query string.
|
|
7
|
+
url.searchParams.set("qs", window.location.search);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
// Remove any existing (potentially stale) query string parameter if the current location doesn't have one
|
|
11
|
+
url.searchParams.delete("qs");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
2
14
|
export function createReconnectingWebSocket(props) {
|
|
3
15
|
const { interval, maxInterval, maxRetries, backoffMultiplier } = props;
|
|
4
16
|
let retries = 0;
|
|
@@ -10,6 +22,7 @@ export function createReconnectingWebSocket(props) {
|
|
|
10
22
|
if (closed) {
|
|
11
23
|
return;
|
|
12
24
|
}
|
|
25
|
+
syncBrowserLocation(props.url);
|
|
13
26
|
socket.current = new WebSocket(props.url);
|
|
14
27
|
socket.current.onopen = () => {
|
|
15
28
|
everConnected = true;
|
package/dist/websocket.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AACA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,MAAM,UAAU,2BAA2B,CACzC,KAAuC;IAEvC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;IACvE,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,eAAe,GAAG,QAAQ,CAAC;IAC/B,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,MAAM,MAAM,GAAG,KAAK,CAAC;IACrB,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;QACT,CAAC;QACD,MAAM,CAAC,OAAO,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE;YAC3B,aAAa,GAAG,IAAI,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACvB,eAAe,GAAG,QAAQ,CAAC;YAC3B,OAAO,GAAG,CAAC,CAAC;YACZ,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,KAAK,CAAC,MAAM,EAAE,CAAC;YACjB,CAAC;QACH,CAAC,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;YACnC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;gBACpB,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE;YAC5B,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBAClB,KAAK,CAAC,OAAO,EAAE,CAAC;YAClB,CAAC;YACD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,GAAG,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;gBAC/B,OAAO;YACT,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC1B,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;gBAC9C,OAAO;YACT,CAAC;YACD,GAAG,CAAC,IAAI,CACN,mBAAmB,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CACxE,CAAC;YACF,UAAU,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACrC,eAAe,GAAG,YAAY,CAC5B,eAAe,EACf,iBAAiB,EACjB,WAAW,CACZ,CAAC;YACF,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE5E,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,eAAuB,EACvB,iBAAyB,EACzB,WAAmB;IAEnB,OAAO,IAAI,CAAC,GAAG;IACb,0CAA0C;IAC1C,eAAe,GAAG,iBAAiB;IACnC,4BAA4B;IAC5B,WAAW,CACZ,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/components.tsx
CHANGED
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
type TargetedEvent,
|
|
9
9
|
} from "preact";
|
|
10
10
|
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
|
11
|
+
import {
|
|
12
|
+
HANDLER_DEBOUNCE,
|
|
13
|
+
HANDLER_MARKER,
|
|
14
|
+
HANDLER_THROTTLE,
|
|
15
|
+
isValidDebounce,
|
|
16
|
+
type TaggedEventHandler,
|
|
17
|
+
} from "./handler";
|
|
11
18
|
import type {
|
|
12
19
|
ImportSourceBinding,
|
|
13
20
|
ReactPyComponent,
|
|
@@ -18,6 +25,117 @@ import type { ReactPyClient } from "./client";
|
|
|
18
25
|
|
|
19
26
|
const ClientContext = createContext<ReactPyClient>(null as any);
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Wrap ``handler`` so its outgoing call is throttled to at most once per
|
|
30
|
+
* ``intervalMs`` milliseconds. Subsequent calls inside the window are
|
|
31
|
+
* collapsed and the trailing call (with the most recent arguments) fires
|
|
32
|
+
* once the window expires.
|
|
33
|
+
*
|
|
34
|
+
* Returns the original handler unchanged when ``intervalMs`` is missing or
|
|
35
|
+
* invalid.
|
|
36
|
+
*/
|
|
37
|
+
function throttleHandler(
|
|
38
|
+
handler: TaggedEventHandler,
|
|
39
|
+
intervalMs: number,
|
|
40
|
+
): TaggedEventHandler {
|
|
41
|
+
if (!isValidDebounce(intervalMs)) {
|
|
42
|
+
return handler;
|
|
43
|
+
}
|
|
44
|
+
let pendingArgs: any[] | null = null;
|
|
45
|
+
let timer: number | null = null;
|
|
46
|
+
let lastFireTime = 0;
|
|
47
|
+
|
|
48
|
+
const wrapped = function (...args: any[]) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const elapsed = now - lastFireTime;
|
|
51
|
+
if (elapsed >= intervalMs) {
|
|
52
|
+
// Leading edge: fire immediately, then start a cooldown.
|
|
53
|
+
lastFireTime = now;
|
|
54
|
+
(handler as (...a: any[]) => void)(...args);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Trailing edge: remember the latest args and schedule a fire at the
|
|
58
|
+
// end of the cooldown so no event is silently dropped.
|
|
59
|
+
pendingArgs = args;
|
|
60
|
+
if (timer === null) {
|
|
61
|
+
timer = window.setTimeout(
|
|
62
|
+
() => {
|
|
63
|
+
timer = null;
|
|
64
|
+
if (pendingArgs !== null) {
|
|
65
|
+
const callArgs = pendingArgs;
|
|
66
|
+
pendingArgs = null;
|
|
67
|
+
lastFireTime = Date.now();
|
|
68
|
+
(handler as (...a: any[]) => void)(...callArgs);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
Math.max(0, intervalMs - elapsed),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} as TaggedEventHandler;
|
|
75
|
+
|
|
76
|
+
// Preserve the tag markers so downstream code can still introspect the
|
|
77
|
+
// wrapped function.
|
|
78
|
+
wrapped[HANDLER_MARKER] = true;
|
|
79
|
+
const debounce = handler[HANDLER_DEBOUNCE];
|
|
80
|
+
if (typeof debounce === "number") {
|
|
81
|
+
wrapped[HANDLER_DEBOUNCE] = debounce;
|
|
82
|
+
}
|
|
83
|
+
return wrapped;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wrap ``handler`` so its outgoing call is debounced by
|
|
88
|
+
* ``delayMs`` milliseconds. The first call schedules a fire after
|
|
89
|
+
* the delay; subsequent calls inside the window reset the timer and
|
|
90
|
+
* pass their arguments to the trailing call. Only one call lands
|
|
91
|
+
* on the wrapped handler per debounce window.
|
|
92
|
+
*
|
|
93
|
+
* Returns the original handler unchanged when ``delayMs`` is missing
|
|
94
|
+
* or invalid, so callers can pass a 0 / negative value to opt out.
|
|
95
|
+
*
|
|
96
|
+
* Unlike ``throttleHandler`` (which fires on the leading edge),
|
|
97
|
+
* this strictly trailing-edge debounce is the right semantics for
|
|
98
|
+
* text inputs: the server only sees the final state after the user
|
|
99
|
+
* pauses typing, which is exactly when the reconcile can safely apply
|
|
100
|
+
* a server-side transformation (normalisation, validation, etc.).
|
|
101
|
+
*/
|
|
102
|
+
function debounceHandler(
|
|
103
|
+
handler: TaggedEventHandler,
|
|
104
|
+
delayMs: number,
|
|
105
|
+
): TaggedEventHandler {
|
|
106
|
+
if (!isValidDebounce(delayMs) || delayMs === 0) {
|
|
107
|
+
return handler;
|
|
108
|
+
}
|
|
109
|
+
let timer: number | null = null;
|
|
110
|
+
let pendingArgs: any[] | null = null;
|
|
111
|
+
|
|
112
|
+
const wrapped = function (...args: any[]) {
|
|
113
|
+
pendingArgs = args;
|
|
114
|
+
if (timer !== null) {
|
|
115
|
+
window.clearTimeout(timer);
|
|
116
|
+
}
|
|
117
|
+
timer = window.setTimeout(() => {
|
|
118
|
+
timer = null;
|
|
119
|
+
if (pendingArgs !== null) {
|
|
120
|
+
const callArgs = pendingArgs;
|
|
121
|
+
pendingArgs = null;
|
|
122
|
+
(handler as (...a: any[]) => void)(...callArgs);
|
|
123
|
+
}
|
|
124
|
+
}, delayMs);
|
|
125
|
+
} as TaggedEventHandler;
|
|
126
|
+
|
|
127
|
+
// Preserve the tag markers so downstream code can still introspect
|
|
128
|
+
// the wrapped function.
|
|
129
|
+
wrapped[HANDLER_MARKER] = true;
|
|
130
|
+
if (typeof handler[HANDLER_THROTTLE] === "number") {
|
|
131
|
+
wrapped[HANDLER_THROTTLE] = handler[HANDLER_THROTTLE];
|
|
132
|
+
}
|
|
133
|
+
if (typeof handler[HANDLER_DEBOUNCE] === "number") {
|
|
134
|
+
wrapped[HANDLER_DEBOUNCE] = handler[HANDLER_DEBOUNCE];
|
|
135
|
+
}
|
|
136
|
+
return wrapped;
|
|
137
|
+
}
|
|
138
|
+
|
|
21
139
|
export function Layout(props: { client: ReactPyClient }): JSX.Element {
|
|
22
140
|
const currentModel: ReactPyVdom = useState({ tagName: "" })[0];
|
|
23
141
|
const forceUpdate = useForceUpdate();
|
|
@@ -66,12 +184,32 @@ export function Element({ model }: { model: ReactPyVdom }): JSX.Element | null {
|
|
|
66
184
|
|
|
67
185
|
function StandardElement({ model }: { model: ReactPyVdom }) {
|
|
68
186
|
const client = useContext(ClientContext);
|
|
187
|
+
const attrs = createAttributes(model, client);
|
|
188
|
+
// Apply the throttle wrapper to tagged handlers. ``throttle`` works on
|
|
189
|
+
// every element; ``debounce`` is intentionally only applied by
|
|
190
|
+
// ``UserInputElement`` (the only place where coalescing keystrokes
|
|
191
|
+
// is meaningful — click handlers don't benefit from trailing-edge
|
|
192
|
+
// coalescing, and applying debounce here would just delay
|
|
193
|
+
// non-input events for no reason).
|
|
194
|
+
for (const [name, prop] of Object.entries(attrs)) {
|
|
195
|
+
if (typeof prop !== "function") {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const handler = prop as TaggedEventHandler;
|
|
199
|
+
if (!handler[HANDLER_MARKER]) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const throttle = handler[HANDLER_THROTTLE];
|
|
203
|
+
if (isValidDebounce(throttle)) {
|
|
204
|
+
attrs[name] = throttleHandler(handler, throttle as number);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
69
207
|
// Use createElement here to avoid warning about variable numbers of children not
|
|
70
208
|
// having keys. Warning about this must now be the responsibility of the client
|
|
71
209
|
// providing the models instead of the client rendering them.
|
|
72
210
|
return createElement(
|
|
73
211
|
model.tagName === "" ? Fragment : model.tagName,
|
|
74
|
-
|
|
212
|
+
attrs,
|
|
75
213
|
...createChildren(model, (child) => {
|
|
76
214
|
return <Element model={child} key={child.attributes?.key} />;
|
|
77
215
|
}),
|
|
@@ -81,30 +219,195 @@ function StandardElement({ model }: { model: ReactPyVdom }) {
|
|
|
81
219
|
function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element {
|
|
82
220
|
const client = useContext(ClientContext);
|
|
83
221
|
const props = createAttributes(model, client);
|
|
84
|
-
|
|
222
|
+
// ``_reactpy_ack_seq`` is set by the server to the highest sequence
|
|
223
|
+
// number it has received from this element's event handlers. We use
|
|
224
|
+
// it (instead of a time-based debounce) to decide whether the server
|
|
225
|
+
// has caught up to the user's keystrokes.
|
|
226
|
+
const serverAckSeq =
|
|
227
|
+
typeof model.attributes?.["_reactpy_ack_seq"] === "number"
|
|
228
|
+
? (model.attributes["_reactpy_ack_seq"] as number)
|
|
229
|
+
: -1;
|
|
230
|
+
// Strip the internal key from props so it never reaches the DOM.
|
|
231
|
+
delete (props as Record<string, unknown>)["_reactpy_ack_seq"];
|
|
232
|
+
|
|
233
|
+
const [, setValue] = useState(props.value);
|
|
234
|
+
// Reference to the underlying DOM element. We read its current
|
|
235
|
+
// ``value`` from the reconcile effect to compare against the
|
|
236
|
+
// server's proposed value — reading from Preact state is not
|
|
237
|
+
// enough because the browser mutates the DOM directly between
|
|
238
|
+
// renders (especially during fast typing).
|
|
239
|
+
const inputRef = useRef<
|
|
240
|
+
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null
|
|
241
|
+
>(null);
|
|
242
|
+
// Per-element (NOT per-handler) monotonic counter for outgoing
|
|
243
|
+
// events. The wrapper below installs this counter onto every
|
|
244
|
+
// handler via ``_reactpy_set_seq`` so the counter survives
|
|
245
|
+
// handler recreation on every server re-render. Each handler
|
|
246
|
+
// has its own per-handler ``outgoingSeq`` (used as the default
|
|
247
|
+
// when no wrapper is installed), but we override it here so the
|
|
248
|
+
// element owns a single monotonic counter across handlers.
|
|
249
|
+
const sharedOutgoingSeq = useRef(0);
|
|
250
|
+
// Highest sequence number actually sent. The server's
|
|
251
|
+
// ``_reactpy_ack_seq`` will catch up to this. ``sharedOutgoingSeq``
|
|
252
|
+
// is incremented optimistically in the wrapper; ``lastSentSeq``
|
|
253
|
+
// is the high-water mark.
|
|
254
|
+
const lastSentSeq = useRef(-1);
|
|
85
255
|
|
|
86
256
|
// honor changes to value from the client via props
|
|
87
|
-
useEffect(() =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
// The sequence number is the single source of truth for whether
|
|
259
|
+
// to apply the server's value. Time-based heuristics (debounce
|
|
260
|
+
// windows, submit-event detection) are not used here because they
|
|
261
|
+
// cannot reliably distinguish a server snapshot from before some
|
|
262
|
+
// keystrokes were processed from a snapshot taken after all
|
|
263
|
+
// keystrokes were processed. The sequence number can,
|
|
264
|
+
// deterministically.
|
|
265
|
+
//
|
|
266
|
+
// If the server has acknowledged every event the user has sent
|
|
267
|
+
// (``serverAckSeq >= lastSentSeq.current``), the server's value
|
|
268
|
+
// is the authoritative one and is applied directly. This
|
|
269
|
+
// includes clears (Enter handlers that reset the input),
|
|
270
|
+
// normalizations, and same-value confirmations.
|
|
271
|
+
// If the server is behind, its value is necessarily a stale
|
|
272
|
+
// snapshot and is ignored; the next layout-update will be
|
|
273
|
+
// applied once the server catches up.
|
|
274
|
+
//
|
|
275
|
+
// We additionally compare against the DOM's actual current
|
|
276
|
+
// value via ``inputRef`` — when the server is supposedly
|
|
277
|
+
// caught up but its snapshot is shorter than what the user
|
|
278
|
+
// has in the DOM (a stale snapshot racing with the user's
|
|
279
|
+
// most recent keystroke), skip applying so we don't clobber
|
|
280
|
+
// the user's text. This handles the realistic case where the
|
|
281
|
+
// user types faster than the server can ack.
|
|
282
|
+
if (serverAckSeq < lastSentSeq.current) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Apply server's value to the DOM directly via the ref, NOT
|
|
286
|
+
// through Preact's render path. Preact would otherwise set
|
|
287
|
+
// ``inputRef.current.value`` on every render, racing with the
|
|
288
|
+
// browser's own mutations of the DOM value during fast typing
|
|
289
|
+
// and silently dropping keystrokes. By using a ref and writing
|
|
290
|
+
// only when we know the server has caught up, we let the
|
|
291
|
+
// browser manage the DOM value during typing and only override
|
|
292
|
+
// it when it's safe to do so.
|
|
293
|
+
//
|
|
294
|
+
// Crucially, only write when ``props.value`` is a real string.
|
|
295
|
+
// Inputs without a ``value`` attribute in their VDOM (e.g.
|
|
296
|
+
// uncontrolled inputs in the user_data and channel_layer tests)
|
|
297
|
+
// arrive with ``props.value === undefined``; assigning
|
|
298
|
+
// ``input.value = undefined`` coerces to the literal string
|
|
299
|
+
// ``"undefined"`` and seeds the field with garbage that the
|
|
300
|
+
// user's first keystroke will then append to (``test`` becomes
|
|
301
|
+
// ``testundefined``). Skipping the write in that case leaves
|
|
302
|
+
// the DOM at its default empty value, which is what the user
|
|
303
|
+
// actually typed into.
|
|
304
|
+
if (
|
|
305
|
+
inputRef.current &&
|
|
306
|
+
typeof inputRef.current.value === "string" &&
|
|
307
|
+
typeof props.value === "string" &&
|
|
308
|
+
inputRef.current.value !== props.value
|
|
309
|
+
) {
|
|
310
|
+
inputRef.current.value = props.value;
|
|
311
|
+
}
|
|
312
|
+
setValue(props.value);
|
|
313
|
+
}, [props.value, serverAckSeq]);
|
|
314
|
+
|
|
315
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
316
|
+
if (typeof prop !== "function") {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const givenHandler = prop as TaggedEventHandler;
|
|
321
|
+
if (!givenHandler[HANDLER_MARKER]) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Apply ``throttle`` and/or ``debounce`` wrappers when the
|
|
326
|
+
// handler opts into them. Throttle fires on the leading edge
|
|
327
|
+
// (good for click-style events); debounce fires on the
|
|
328
|
+
// trailing edge (good for typing, where you want the server
|
|
329
|
+
// to see only the final state after the user pauses). Both
|
|
330
|
+
// are no-ops when the corresponding keyword is absent, which
|
|
331
|
+
// is the common case — the per-element seq reconcile is
|
|
332
|
+
// already responsible for input coherency, so we don't apply
|
|
333
|
+
// a debounce by default.
|
|
334
|
+
const handlerDebounce = givenHandler[HANDLER_DEBOUNCE];
|
|
335
|
+
const handlerThrottle = givenHandler[HANDLER_THROTTLE];
|
|
336
|
+
let wrapped: TaggedEventHandler = givenHandler;
|
|
337
|
+
if (isValidDebounce(handlerThrottle)) {
|
|
338
|
+
wrapped = throttleHandler(wrapped, handlerThrottle as number);
|
|
339
|
+
}
|
|
340
|
+
if (isValidDebounce(handlerDebounce)) {
|
|
341
|
+
wrapped = debounceHandler(wrapped, handlerDebounce as number);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
props[name] = (event: TargetedEvent<any>) => {
|
|
345
|
+
// Use a per-element (shared across all handlers on this
|
|
346
|
+
// element) monotonic counter for outgoing events. We
|
|
347
|
+
// overwrite the handler's own ``outgoingSeq`` with this
|
|
348
|
+
// counter so the wire-format seq number reflects the
|
|
349
|
+
// element-wide sequence. The handler closure's own
|
|
350
|
+
// ``outgoingSeq`` is unused for sequencing purposes now
|
|
351
|
+
// (it still exists as a default for non-wrapped callers).
|
|
352
|
+
//
|
|
353
|
+
// Critically, we increment the seq counter and update
|
|
354
|
+
// ``lastSentSeq`` on every *user* event, not on every
|
|
355
|
+
// server dispatch. If the handler is wrapped in a
|
|
356
|
+
// debounce that coalesces several events into one server
|
|
357
|
+
// dispatch, ``lastSentSeq`` will be higher than the seq
|
|
358
|
+
// actually sent to the server, which is fine — the seq
|
|
359
|
+
// we sent is the LATEST user-typed value, which is the
|
|
360
|
+
// one the server actually processed.
|
|
361
|
+
const seq = sharedOutgoingSeq.current++;
|
|
362
|
+
if (seq > lastSentSeq.current) {
|
|
363
|
+
lastSentSeq.current = seq;
|
|
95
364
|
}
|
|
96
|
-
|
|
97
|
-
|
|
365
|
+
const taggedHandler = givenHandler as TaggedEventHandler & {
|
|
366
|
+
_reactpy_set_seq?: (n: number) => void;
|
|
367
|
+
};
|
|
368
|
+
if (typeof taggedHandler._reactpy_set_seq === "function") {
|
|
369
|
+
// Push the handler's internal counter past our seq so
|
|
370
|
+
// the next (possibly debounced) dispatch uses a seq
|
|
371
|
+
// number that matches what the user just typed, not
|
|
372
|
+
// whatever stale value the handler closure happened
|
|
373
|
+
// to have left over.
|
|
374
|
+
taggedHandler._reactpy_set_seq(seq + 1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ``onKeyPress`` fires before the DOM has been updated with
|
|
378
|
+
// the new keystroke — ``event.target.value`` is the value
|
|
379
|
+
// BEFORE the character was added. We deliberately do NOT
|
|
380
|
+
// trust it for value-tracking. ``onChange``/``onInput`` fire
|
|
381
|
+
// after the DOM has been updated and can be trusted, but we
|
|
382
|
+
// don't even need to track it separately here — the
|
|
383
|
+
// reconcile effect reads the DOM directly via ``inputRef``
|
|
384
|
+
// so it always sees the post-keystroke value.
|
|
385
|
+
|
|
386
|
+
wrapped(event);
|
|
98
387
|
};
|
|
99
388
|
}
|
|
100
389
|
|
|
101
390
|
// Use createElement here to avoid warning about variable numbers of children not
|
|
102
391
|
// having keys. Warning about this must now be the responsibility of the client
|
|
103
392
|
// providing the models instead of the client rendering them.
|
|
393
|
+
// Drop ``value`` from the props we pass to Preact — we want the
|
|
394
|
+
// input to be fully uncontrolled. Preact would otherwise set
|
|
395
|
+
// ``inputRef.current.value`` on every render (because ``value``
|
|
396
|
+
// is a known DOM property), racing with the browser's own
|
|
397
|
+
// mutations of the DOM value during fast typing and silently
|
|
398
|
+
// dropping keystrokes. We instead update the DOM value via the
|
|
399
|
+
// ``inputRef`` in the reconcile effect above, only when the
|
|
400
|
+
// server has caught up and the proposed value is not shorter
|
|
401
|
+
// than what the user has already typed.
|
|
402
|
+
const controlledProps: Record<string, any> = {};
|
|
403
|
+
for (const key of Object.keys(props as Record<string, any>)) {
|
|
404
|
+
if (key !== "value") {
|
|
405
|
+
controlledProps[key] = (props as Record<string, any>)[key];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
104
408
|
return createElement(
|
|
105
409
|
model.tagName,
|
|
106
|
-
|
|
107
|
-
{ ...props, value },
|
|
410
|
+
{ ...controlledProps, ref: inputRef },
|
|
108
411
|
...createChildren(model, (child) => (
|
|
109
412
|
<Element model={child} key={child.attributes?.key} />
|
|
110
413
|
)),
|
package/src/handler.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Shared marker property names attached to event handler functions created by
|
|
2
|
+
// `vdom.tsx::createEventHandler`. Centralizing these strings keeps `components.tsx`
|
|
3
|
+
// and `vdom.tsx` in sync without relying on ad-hoc property names.
|
|
4
|
+
|
|
5
|
+
export const HANDLER_MARKER = "isHandler" as const;
|
|
6
|
+
export const HANDLER_DEBOUNCE = "debounce" as const;
|
|
7
|
+
export const HANDLER_THROTTLE = "throttle" as const;
|
|
8
|
+
|
|
9
|
+
export type HandlerMarker = typeof HANDLER_MARKER;
|
|
10
|
+
export type HandlerDebounce = typeof HANDLER_DEBOUNCE;
|
|
11
|
+
export type HandlerThrottle = typeof HANDLER_THROTTLE;
|
|
12
|
+
|
|
13
|
+
export type TaggedEventHandler = ((event: Event) => void) & {
|
|
14
|
+
[HANDLER_MARKER]: true;
|
|
15
|
+
[HANDLER_DEBOUNCE]?: number;
|
|
16
|
+
[HANDLER_THROTTLE]?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns true when the given value is a finite, non-negative integer.
|
|
21
|
+
* Used to validate debounce/throttle values arriving over the wire from the
|
|
22
|
+
* Python layout.
|
|
23
|
+
*/
|
|
24
|
+
export function isValidDebounce(value: unknown): value is number {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === "number" &&
|
|
27
|
+
Number.isFinite(value) &&
|
|
28
|
+
Number.isInteger(value) &&
|
|
29
|
+
value >= 0
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const isValidThrottle = isValidDebounce;
|
package/src/index.ts
CHANGED
package/src/mount.tsx
CHANGED
|
@@ -12,12 +12,9 @@ export function mountReactPy(props: MountProps) {
|
|
|
12
12
|
);
|
|
13
13
|
|
|
14
14
|
// Embed the initial HTTP path into the WebSocket URL
|
|
15
|
-
componentUrl.searchParams.append("
|
|
15
|
+
componentUrl.searchParams.append("path", window.location.pathname);
|
|
16
16
|
if (window.location.search) {
|
|
17
|
-
componentUrl.searchParams.append(
|
|
18
|
-
"http_query_string",
|
|
19
|
-
window.location.search,
|
|
20
|
-
);
|
|
17
|
+
componentUrl.searchParams.append("qs", window.location.search);
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
// Configure a new ReactPy client
|
package/src/types.ts
CHANGED
|
@@ -48,7 +48,7 @@ export type ReactPyComponent = ComponentType<{ model: ReactPyVdom }>;
|
|
|
48
48
|
|
|
49
49
|
export type ReactPyVdom = {
|
|
50
50
|
tagName: string;
|
|
51
|
-
attributes?: { [key: string]:
|
|
51
|
+
attributes?: { [key: string]: any };
|
|
52
52
|
children?: (ReactPyVdom | string)[];
|
|
53
53
|
error?: string;
|
|
54
54
|
eventHandlers?: { [key: string]: ReactPyVdomEventHandler };
|
|
@@ -60,6 +60,8 @@ export type ReactPyVdomEventHandler = {
|
|
|
60
60
|
target: string;
|
|
61
61
|
preventDefault?: boolean;
|
|
62
62
|
stopPropagation?: boolean;
|
|
63
|
+
debounce?: number;
|
|
64
|
+
throttle?: number;
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
export type ReactPyVdomImportSource = {
|
package/src/vdom.tsx
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import eventToObject from "event-to-object";
|
|
2
2
|
import { Fragment } from "preact";
|
|
3
|
+
import {
|
|
4
|
+
HANDLER_DEBOUNCE,
|
|
5
|
+
HANDLER_MARKER,
|
|
6
|
+
HANDLER_THROTTLE,
|
|
7
|
+
isValidDebounce,
|
|
8
|
+
isValidThrottle,
|
|
9
|
+
type TaggedEventHandler,
|
|
10
|
+
} from "./handler";
|
|
11
|
+
import { infer_bind_from_environment } from "./bind";
|
|
12
|
+
import log from "./logger";
|
|
13
|
+
import type { ReactPyClient } from "./client";
|
|
3
14
|
import type {
|
|
4
|
-
ReactPyVdom,
|
|
5
|
-
ReactPyVdomImportSource,
|
|
6
|
-
ReactPyVdomEventHandler,
|
|
7
|
-
ReactPyModule,
|
|
8
15
|
BindImportSource,
|
|
9
|
-
ReactPyModuleBinding,
|
|
10
16
|
ImportSourceBinding,
|
|
17
|
+
ReactPyModule,
|
|
18
|
+
ReactPyModuleBinding,
|
|
19
|
+
ReactPyVdom,
|
|
20
|
+
ReactPyVdomEventHandler,
|
|
21
|
+
ReactPyVdomImportSource,
|
|
11
22
|
} from "./types";
|
|
12
|
-
import { infer_bind_from_environment } from "./bind";
|
|
13
|
-
import log from "./logger";
|
|
14
|
-
import type { ReactPyClient } from "./client";
|
|
15
23
|
|
|
16
24
|
export async function loadImportSource(
|
|
17
25
|
vdomImportSource: ReactPyVdomImportSource,
|
|
@@ -206,8 +214,22 @@ export function createAttributes(
|
|
|
206
214
|
function createEventHandler(
|
|
207
215
|
client: ReactPyClient,
|
|
208
216
|
name: string,
|
|
209
|
-
{
|
|
210
|
-
|
|
217
|
+
{
|
|
218
|
+
target,
|
|
219
|
+
preventDefault,
|
|
220
|
+
stopPropagation,
|
|
221
|
+
debounce,
|
|
222
|
+
throttle,
|
|
223
|
+
}: ReactPyVdomEventHandler,
|
|
224
|
+
): [string, TaggedEventHandler] {
|
|
225
|
+
// Sequence number for the next outgoing event on this handler.
|
|
226
|
+
// The wrapper in ``UserInputElement`` may overwrite this before
|
|
227
|
+
// the event is actually sent (so that the wrapper, not this
|
|
228
|
+
// closure, owns the per-element monotonic counter and survives
|
|
229
|
+
// handler recreation on every server re-render). The default
|
|
230
|
+
// behavior (no wrapper) is to use a per-handler counter starting
|
|
231
|
+
// at 0.
|
|
232
|
+
let outgoingSeq = 0;
|
|
211
233
|
const eventHandler = function (...args: any[]) {
|
|
212
234
|
const data = Array.from(args).map((value) => {
|
|
213
235
|
const event = value as Event;
|
|
@@ -225,9 +247,37 @@ function createEventHandler(
|
|
|
225
247
|
return event;
|
|
226
248
|
}
|
|
227
249
|
});
|
|
228
|
-
|
|
250
|
+
const seq = outgoingSeq++;
|
|
251
|
+
client.sendMessage({ type: "layout-event", data, target, seq });
|
|
252
|
+
} as TaggedEventHandler & {
|
|
253
|
+
_reactpy_peek_seq?: () => number;
|
|
254
|
+
_reactpy_set_seq?: (n: number) => void;
|
|
229
255
|
};
|
|
230
|
-
eventHandler
|
|
256
|
+
eventHandler[HANDLER_MARKER] = true;
|
|
257
|
+
eventHandler._reactpy_peek_seq = (): number => outgoingSeq;
|
|
258
|
+
eventHandler._reactpy_set_seq = (n: number): void => {
|
|
259
|
+
outgoingSeq = n;
|
|
260
|
+
};
|
|
261
|
+
if (debounce !== undefined) {
|
|
262
|
+
if (!isValidDebounce(debounce)) {
|
|
263
|
+
log.warn(
|
|
264
|
+
`Ignoring invalid debounce value ${JSON.stringify(debounce)} ` +
|
|
265
|
+
`on event handler "${name}": expected a non-negative integer.`,
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
eventHandler[HANDLER_DEBOUNCE] = debounce;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (throttle !== undefined) {
|
|
272
|
+
if (!isValidThrottle(throttle)) {
|
|
273
|
+
log.warn(
|
|
274
|
+
`Ignoring invalid throttle value ${JSON.stringify(throttle)} ` +
|
|
275
|
+
`on event handler "${name}": expected a non-negative integer.`,
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
eventHandler[HANDLER_THROTTLE] = throttle;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
231
281
|
return [name, eventHandler];
|
|
232
282
|
}
|
|
233
283
|
|
package/src/websocket.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { CreateReconnectingWebSocketProps } from "./types";
|
|
2
2
|
import log from "./logger";
|
|
3
3
|
|
|
4
|
+
function syncBrowserLocation(url: URL): void {
|
|
5
|
+
// The window will always have a HTTP path, so ReactPy should always be aware of it.
|
|
6
|
+
url.searchParams.set("path", window.location.pathname);
|
|
7
|
+
|
|
8
|
+
if (window.location.search) {
|
|
9
|
+
// Set the query string parameter if the HTTP location has a query string.
|
|
10
|
+
url.searchParams.set("qs", window.location.search);
|
|
11
|
+
} else {
|
|
12
|
+
// Remove any existing (potentially stale) query string parameter if the current location doesn't have one
|
|
13
|
+
url.searchParams.delete("qs");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
export function createReconnectingWebSocket(
|
|
5
18
|
props: CreateReconnectingWebSocketProps,
|
|
6
19
|
) {
|
|
@@ -15,6 +28,7 @@ export function createReconnectingWebSocket(
|
|
|
15
28
|
if (closed) {
|
|
16
29
|
return;
|
|
17
30
|
}
|
|
31
|
+
syncBrowserLocation(props.url);
|
|
18
32
|
socket.current = new WebSocket(props.url);
|
|
19
33
|
socket.current.onopen = () => {
|
|
20
34
|
everConnected = true;
|