@reactpy/client 0.2.1 → 0.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 +10 -14
- package/dist/messages.d.ts +1 -10
- package/dist/messages.d.ts.map +1 -1
- package/dist/reactpy-client.d.ts +30 -24
- package/dist/reactpy-client.d.ts.map +1 -1
- package/dist/reactpy-client.js +48 -54
- package/dist/reactpy-vdom.js +2 -2
- package/package.json +1 -4
- package/src/components.tsx +7 -11
- package/src/messages.ts +1 -16
- package/src/reactpy-client.ts +77 -90
- package/src/reactpy-vdom.tsx +2 -2
package/dist/components.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":";AAaA,OAAO,EACL,WAAW,EAMZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAIjD,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,GAAG,GAAG,CAAC,OAAO,CAsBpE;AAED,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAoB7E"}
|
package/dist/components.js
CHANGED
|
@@ -6,19 +6,15 @@ const ClientContext = createContext(null);
|
|
|
6
6
|
export function Layout(props) {
|
|
7
7
|
const currentModel = useState({ tagName: "" })[0];
|
|
8
8
|
const forceUpdate = useForceUpdate();
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
props.client.start();
|
|
20
|
-
return () => props.client.stop();
|
|
21
|
-
}, [currentModel, props.client]);
|
|
9
|
+
useEffect(() => props.client.onMessage("layout-update", ({ path, model }) => {
|
|
10
|
+
if (path === "") {
|
|
11
|
+
Object.assign(currentModel, model);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
setJsonPointer(currentModel, path, model);
|
|
15
|
+
}
|
|
16
|
+
forceUpdate();
|
|
17
|
+
}), [currentModel, props.client]);
|
|
22
18
|
return (React.createElement(ClientContext.Provider, { value: props.client },
|
|
23
19
|
React.createElement(Element, { model: currentModel })));
|
|
24
20
|
}
|
|
@@ -94,7 +90,7 @@ function ScriptElement({ model }) {
|
|
|
94
90
|
ref.current.appendChild(scriptElement);
|
|
95
91
|
}
|
|
96
92
|
else if (scriptContent) {
|
|
97
|
-
|
|
93
|
+
const scriptResult = eval(scriptContent);
|
|
98
94
|
if (typeof scriptResult == "function") {
|
|
99
95
|
return scriptResult();
|
|
100
96
|
}
|
package/dist/messages.d.ts
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
import { ReactPyVdom } from "./reactpy-vdom";
|
|
2
|
-
export interface IMessage {
|
|
3
|
-
type: string;
|
|
4
|
-
}
|
|
5
|
-
export type ConnectionOpenMessage = {
|
|
6
|
-
type: "connection-open";
|
|
7
|
-
};
|
|
8
|
-
export type ConnectionCloseMessage = {
|
|
9
|
-
type: "connection-close";
|
|
10
|
-
};
|
|
11
2
|
export type LayoutUpdateMessage = {
|
|
12
3
|
type: "layout-update";
|
|
13
4
|
path: string;
|
|
@@ -18,7 +9,7 @@ export type LayoutEventMessage = {
|
|
|
18
9
|
target: string;
|
|
19
10
|
data: any;
|
|
20
11
|
};
|
|
21
|
-
export type IncomingMessage = LayoutUpdateMessage
|
|
12
|
+
export type IncomingMessage = LayoutUpdateMessage;
|
|
22
13
|
export type OutgoingMessage = LayoutEventMessage;
|
|
23
14
|
export type Message = IncomingMessage | OutgoingMessage;
|
|
24
15
|
//# sourceMappingURL=messages.d.ts.map
|
package/dist/messages.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,GAAG,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAClD,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AACjD,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,eAAe,CAAC"}
|
package/dist/reactpy-client.d.ts
CHANGED
|
@@ -1,33 +1,46 @@
|
|
|
1
|
-
import { OutgoingMessage, IncomingMessage } from "./messages";
|
|
2
1
|
import { ReactPyModule } from "./reactpy-vdom";
|
|
3
2
|
/**
|
|
4
3
|
* A client for communicating with a ReactPy server.
|
|
5
4
|
*/
|
|
6
5
|
export interface ReactPyClient {
|
|
7
|
-
/**
|
|
8
|
-
* Connect to the server and start receiving messages.
|
|
9
|
-
*
|
|
10
|
-
* Message handlers should be registered before calling this method in order to
|
|
11
|
-
* garuntee that messages are not missed.
|
|
12
|
-
*/
|
|
13
|
-
start: () => void;
|
|
14
|
-
/**
|
|
15
|
-
* Disconnect from the server and stop receiving messages.
|
|
16
|
-
*/
|
|
17
|
-
stop: () => void;
|
|
18
6
|
/**
|
|
19
7
|
* Register a handler for a message type.
|
|
8
|
+
*
|
|
9
|
+
* The first time this is called, the client will be considered ready.
|
|
10
|
+
*
|
|
11
|
+
* @param type The type of message to handle.
|
|
12
|
+
* @param handler The handler to call when a message of the given type is received.
|
|
13
|
+
* @returns A function to unregister the handler.
|
|
20
14
|
*/
|
|
21
|
-
onMessage
|
|
15
|
+
onMessage(type: string, handler: (message: any) => void): () => void;
|
|
22
16
|
/**
|
|
23
17
|
* Send a message to the server.
|
|
18
|
+
*
|
|
19
|
+
* @param message The message to send. Messages must have a `type` property.
|
|
24
20
|
*/
|
|
25
|
-
sendMessage: (message:
|
|
21
|
+
sendMessage: (message: any) => void;
|
|
26
22
|
/**
|
|
27
|
-
*
|
|
23
|
+
* Load a module from the server.
|
|
24
|
+
* @param moduleName The name of the module to load.
|
|
25
|
+
* @returns A promise that resolves to the module.
|
|
28
26
|
*/
|
|
29
27
|
loadModule: (moduleName: string) => Promise<ReactPyModule>;
|
|
30
28
|
}
|
|
29
|
+
export declare class BaseReactPyClient implements Partial<ReactPyClient> {
|
|
30
|
+
private readonly handlers;
|
|
31
|
+
protected readonly ready: Promise<void>;
|
|
32
|
+
private resolveReady;
|
|
33
|
+
constructor();
|
|
34
|
+
onMessage(type: string, handler: (message: any) => void): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Handle an incoming message.
|
|
37
|
+
*
|
|
38
|
+
* This should be called by subclasses when a message is received.
|
|
39
|
+
*
|
|
40
|
+
* @param message The message to handle. The message must have a `type` property.
|
|
41
|
+
*/
|
|
42
|
+
protected handleIncoming(message: any): void;
|
|
43
|
+
}
|
|
31
44
|
export type SimpleReactPyClientProps = {
|
|
32
45
|
serverLocation?: LocationProps;
|
|
33
46
|
reconnectOptions?: ReconnectProps;
|
|
@@ -68,19 +81,12 @@ type ReconnectProps = {
|
|
|
68
81
|
backoffRate?: number;
|
|
69
82
|
intervalJitter?: number;
|
|
70
83
|
};
|
|
71
|
-
export declare class SimpleReactPyClient implements ReactPyClient {
|
|
72
|
-
private resolveShouldOpen;
|
|
73
|
-
private resolveShouldClose;
|
|
84
|
+
export declare class SimpleReactPyClient extends BaseReactPyClient implements ReactPyClient {
|
|
74
85
|
private readonly urls;
|
|
75
|
-
private readonly handlers;
|
|
76
86
|
private readonly socket;
|
|
77
87
|
constructor(props: SimpleReactPyClientProps);
|
|
78
|
-
|
|
79
|
-
stop(): void;
|
|
80
|
-
onMessage<M extends IncomingMessage>(type: M["type"], handler: (message: M) => void): void;
|
|
81
|
-
sendMessage(message: OutgoingMessage): void;
|
|
88
|
+
sendMessage(message: any): void;
|
|
82
89
|
loadModule(moduleName: string): Promise<ReactPyModule>;
|
|
83
|
-
private handleIncoming;
|
|
84
90
|
}
|
|
85
91
|
export {};
|
|
86
92
|
//# sourceMappingURL=reactpy-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactpy-client.d.ts","sourceRoot":"","sources":["../src/reactpy-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"reactpy-client.d.ts","sourceRoot":"","sources":["../src/reactpy-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;;;;OAQG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAErE;;;;OAIG;IACH,WAAW,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IAEpC;;;;OAIG;IACH,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5D;AAED,qBAAa,iBAAkB,YAAW,OAAO,CAAC,aAAa,CAAC;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqD;IAC9E,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,CAAC,YAAY,CAA6B;;IAOjD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI;IAQpE;;;;;;OAMG;IACH,SAAS,CAAC,cAAc,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;CAe7C;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B,gBAAgB,CAAC,EAAE,cAAc,CAAC;CACnC,CAAC;AAEF;;;;;;;;;GASG;AACH,KAAK,aAAa,GAAG;IACnB;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,qBAAa,mBACX,SAAQ,iBACR,YAAW,aAAa;IAExB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBAErC,KAAK,EAAE,wBAAwB;IAmB3C,WAAW,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;IAI/B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;CAGvD"}
|
package/dist/reactpy-client.js
CHANGED
|
@@ -1,68 +1,62 @@
|
|
|
1
1
|
import logger from "./logger";
|
|
2
|
-
export class
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export class BaseReactPyClient {
|
|
3
|
+
handlers = {};
|
|
4
|
+
ready;
|
|
5
|
+
resolveReady;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.resolveReady = () => { };
|
|
8
|
+
this.ready = new Promise((resolve) => (this.resolveReady = resolve));
|
|
9
|
+
}
|
|
10
|
+
onMessage(type, handler) {
|
|
11
|
+
(this.handlers[type] || (this.handlers[type] = [])).push(handler);
|
|
12
|
+
this.resolveReady(undefined);
|
|
13
|
+
return () => {
|
|
14
|
+
this.handlers[type] = this.handlers[type].filter((h) => h !== handler);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Handle an incoming message.
|
|
19
|
+
*
|
|
20
|
+
* This should be called by subclasses when a message is received.
|
|
21
|
+
*
|
|
22
|
+
* @param message The message to handle. The message must have a `type` property.
|
|
23
|
+
*/
|
|
24
|
+
handleIncoming(message) {
|
|
25
|
+
if (!message.type) {
|
|
26
|
+
logger.warn("Received message without type", message);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const messageHandlers = this.handlers[message.type];
|
|
30
|
+
if (!messageHandlers) {
|
|
31
|
+
logger.warn("Received message without handler", message);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
messageHandlers.forEach((h) => h(message));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class SimpleReactPyClient extends BaseReactPyClient {
|
|
5
38
|
urls;
|
|
6
|
-
handlers;
|
|
7
39
|
socket;
|
|
8
40
|
constructor(props) {
|
|
9
|
-
|
|
10
|
-
"connection-open": [],
|
|
11
|
-
"connection-close": [],
|
|
12
|
-
"layout-update": [],
|
|
13
|
-
};
|
|
41
|
+
super();
|
|
14
42
|
this.urls = getServerUrls(props.serverLocation || {
|
|
15
43
|
url: document.location.origin,
|
|
16
44
|
route: document.location.pathname,
|
|
17
45
|
query: document.location.search,
|
|
18
46
|
});
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
this.resolveShouldClose = () => {
|
|
23
|
-
throw new Error("Could not stop client");
|
|
24
|
-
};
|
|
25
|
-
const shouldOpen = new Promise((r) => (this.resolveShouldOpen = r));
|
|
26
|
-
const shouldClose = new Promise((r) => (this.resolveShouldClose = r));
|
|
27
|
-
this.socket = startReconnectingWebSocket({
|
|
28
|
-
shouldOpen,
|
|
29
|
-
shouldClose,
|
|
47
|
+
this.socket = createReconnectingWebSocket({
|
|
48
|
+
readyPromise: this.ready,
|
|
30
49
|
url: this.urls.stream,
|
|
31
|
-
onOpen: () => this.handleIncoming({ type: "connection-open" }),
|
|
32
50
|
onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)),
|
|
33
|
-
onClose: () => this.handleIncoming({ type: "connection-close" }),
|
|
34
51
|
...props.reconnectOptions,
|
|
35
52
|
});
|
|
36
53
|
}
|
|
37
|
-
start() {
|
|
38
|
-
logger.log("starting client...");
|
|
39
|
-
this.resolveShouldOpen(undefined);
|
|
40
|
-
}
|
|
41
|
-
stop() {
|
|
42
|
-
logger.log("stopping client...");
|
|
43
|
-
this.resolveShouldClose(undefined);
|
|
44
|
-
}
|
|
45
|
-
onMessage(type, handler) {
|
|
46
|
-
this.handlers[type].push(handler);
|
|
47
|
-
}
|
|
48
54
|
sendMessage(message) {
|
|
49
55
|
this.socket.current?.send(JSON.stringify(message));
|
|
50
56
|
}
|
|
51
57
|
loadModule(moduleName) {
|
|
52
58
|
return import(`${this.urls.modules}/${moduleName}`);
|
|
53
59
|
}
|
|
54
|
-
handleIncoming(message) {
|
|
55
|
-
if (!message.type) {
|
|
56
|
-
logger.warn("Received message without type", message);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const messageHandlers = this.handlers[message.type];
|
|
60
|
-
if (!messageHandlers) {
|
|
61
|
-
logger.warn("Received message without handler", message);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
messageHandlers.forEach((h) => h(message));
|
|
65
|
-
}
|
|
66
60
|
}
|
|
67
61
|
function getServerUrls(props) {
|
|
68
62
|
const base = new URL(`${props.url || document.location.origin}/_reactpy`);
|
|
@@ -73,12 +67,12 @@ function getServerUrls(props) {
|
|
|
73
67
|
const stream = `${streamProtocol}://${base.host}${streamPath}${props.query}`;
|
|
74
68
|
return { base, modules, assets, stream };
|
|
75
69
|
}
|
|
76
|
-
function
|
|
70
|
+
function createReconnectingWebSocket(props) {
|
|
77
71
|
const { maxInterval = 60000, maxRetries = 50, backoffRate = 1.1, intervalJitter = 0.1, } = props;
|
|
78
72
|
const startInterval = 750;
|
|
79
73
|
let retries = 0;
|
|
80
74
|
let interval = startInterval;
|
|
81
|
-
|
|
75
|
+
const closed = false;
|
|
82
76
|
let everConnected = false;
|
|
83
77
|
const socket = {};
|
|
84
78
|
const connect = () => {
|
|
@@ -91,7 +85,9 @@ function startReconnectingWebSocket(props) {
|
|
|
91
85
|
logger.log("client connected");
|
|
92
86
|
interval = startInterval;
|
|
93
87
|
retries = 0;
|
|
94
|
-
props.onOpen
|
|
88
|
+
if (props.onOpen) {
|
|
89
|
+
props.onOpen();
|
|
90
|
+
}
|
|
95
91
|
};
|
|
96
92
|
socket.current.onmessage = props.onMessage;
|
|
97
93
|
socket.current.onclose = () => {
|
|
@@ -100,7 +96,9 @@ function startReconnectingWebSocket(props) {
|
|
|
100
96
|
return;
|
|
101
97
|
}
|
|
102
98
|
logger.log("client disconnected");
|
|
103
|
-
props.onClose
|
|
99
|
+
if (props.onClose) {
|
|
100
|
+
props.onClose();
|
|
101
|
+
}
|
|
104
102
|
if (retries >= maxRetries) {
|
|
105
103
|
return;
|
|
106
104
|
}
|
|
@@ -111,11 +109,7 @@ function startReconnectingWebSocket(props) {
|
|
|
111
109
|
retries++;
|
|
112
110
|
};
|
|
113
111
|
};
|
|
114
|
-
props.
|
|
115
|
-
props.shouldClose.then(() => {
|
|
116
|
-
closed = true;
|
|
117
|
-
socket.current?.close();
|
|
118
|
-
});
|
|
112
|
+
props.readyPromise.then(() => logger.log("starting client...")).then(connect);
|
|
119
113
|
return socket;
|
|
120
114
|
}
|
|
121
115
|
function nextInterval(currentInterval, backoffRate, maxInterval) {
|
package/dist/reactpy-vdom.js
CHANGED
|
@@ -98,8 +98,8 @@ export function createAttributes(model, client) {
|
|
|
98
98
|
function createEventHandler(client, name, { target, preventDefault, stopPropagation }) {
|
|
99
99
|
return [
|
|
100
100
|
name,
|
|
101
|
-
function () {
|
|
102
|
-
const data = Array.from(
|
|
101
|
+
function (...args) {
|
|
102
|
+
const data = Array.from(args).map((value) => {
|
|
103
103
|
if (!(typeof value === "object" && value.nativeEvent)) {
|
|
104
104
|
return value;
|
|
105
105
|
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"name": "@reactpy/client",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"event-to-object": "^0.1.2",
|
|
12
12
|
"json-pointer": "^0.6.2"
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"@types/json-pointer": "^1.0.31",
|
|
16
16
|
"@types/react": "^17.0",
|
|
17
17
|
"@types/react-dom": "^17.0",
|
|
18
|
-
"prettier": "^3.0.0-alpha.6",
|
|
19
18
|
"typescript": "^4.9.5"
|
|
20
19
|
},
|
|
21
20
|
"peerDependencies": {
|
|
@@ -28,9 +27,7 @@
|
|
|
28
27
|
},
|
|
29
28
|
"scripts": {
|
|
30
29
|
"build": "tsc -b",
|
|
31
|
-
"format": "prettier --write .",
|
|
32
30
|
"test": "npm run check:tests",
|
|
33
|
-
"check:format": "prettier --check .",
|
|
34
31
|
"check:tests": "echo 'no tests'",
|
|
35
32
|
"check:types": "tsc --noEmit"
|
|
36
33
|
}
|
package/src/components.tsx
CHANGED
|
@@ -11,7 +11,6 @@ import React, {
|
|
|
11
11
|
} from "react";
|
|
12
12
|
// @ts-ignore
|
|
13
13
|
import { set as setJsonPointer } from "json-pointer";
|
|
14
|
-
import { LayoutUpdateMessage } from "./messages";
|
|
15
14
|
import {
|
|
16
15
|
ReactPyVdom,
|
|
17
16
|
ReactPyComponent,
|
|
@@ -28,21 +27,18 @@ export function Layout(props: { client: ReactPyClient }): JSX.Element {
|
|
|
28
27
|
const currentModel: ReactPyVdom = useState({ tagName: "" })[0];
|
|
29
28
|
const forceUpdate = useForceUpdate();
|
|
30
29
|
|
|
31
|
-
useEffect(
|
|
32
|
-
|
|
33
|
-
"layout-update",
|
|
34
|
-
({ path, model }) => {
|
|
30
|
+
useEffect(
|
|
31
|
+
() =>
|
|
32
|
+
props.client.onMessage("layout-update", ({ path, model }) => {
|
|
35
33
|
if (path === "") {
|
|
36
34
|
Object.assign(currentModel, model);
|
|
37
35
|
} else {
|
|
38
36
|
setJsonPointer(currentModel, path, model);
|
|
39
37
|
}
|
|
40
38
|
forceUpdate();
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return () => props.client.stop();
|
|
45
|
-
}, [currentModel, props.client]);
|
|
39
|
+
}),
|
|
40
|
+
[currentModel, props.client],
|
|
41
|
+
);
|
|
46
42
|
|
|
47
43
|
return (
|
|
48
44
|
<ClientContext.Provider value={props.client}>
|
|
@@ -140,7 +136,7 @@ function ScriptElement({ model }: { model: ReactPyVdom }) {
|
|
|
140
136
|
}
|
|
141
137
|
ref.current.appendChild(scriptElement);
|
|
142
138
|
} else if (scriptContent) {
|
|
143
|
-
|
|
139
|
+
const scriptResult = eval(scriptContent);
|
|
144
140
|
if (typeof scriptResult == "function") {
|
|
145
141
|
return scriptResult();
|
|
146
142
|
}
|
package/src/messages.ts
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { ReactPyVdom } from "./reactpy-vdom";
|
|
2
2
|
|
|
3
|
-
export interface IMessage {
|
|
4
|
-
type: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export type ConnectionOpenMessage = {
|
|
8
|
-
type: "connection-open";
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type ConnectionCloseMessage = {
|
|
12
|
-
type: "connection-close";
|
|
13
|
-
};
|
|
14
|
-
|
|
15
3
|
export type LayoutUpdateMessage = {
|
|
16
4
|
type: "layout-update";
|
|
17
5
|
path: string;
|
|
@@ -24,9 +12,6 @@ export type LayoutEventMessage = {
|
|
|
24
12
|
data: any;
|
|
25
13
|
};
|
|
26
14
|
|
|
27
|
-
export type IncomingMessage =
|
|
28
|
-
| LayoutUpdateMessage
|
|
29
|
-
| ConnectionOpenMessage
|
|
30
|
-
| ConnectionCloseMessage;
|
|
15
|
+
export type IncomingMessage = LayoutUpdateMessage;
|
|
31
16
|
export type OutgoingMessage = LayoutEventMessage;
|
|
32
17
|
export type Message = IncomingMessage | OutgoingMessage;
|
package/src/reactpy-client.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { OutgoingMessage, IncomingMessage } from "./messages";
|
|
2
1
|
import { ReactPyModule } from "./reactpy-vdom";
|
|
3
2
|
import logger from "./logger";
|
|
4
3
|
|
|
@@ -6,34 +5,74 @@ import logger from "./logger";
|
|
|
6
5
|
* A client for communicating with a ReactPy server.
|
|
7
6
|
*/
|
|
8
7
|
export interface ReactPyClient {
|
|
9
|
-
/**
|
|
10
|
-
* Connect to the server and start receiving messages.
|
|
11
|
-
*
|
|
12
|
-
* Message handlers should be registered before calling this method in order to
|
|
13
|
-
* garuntee that messages are not missed.
|
|
14
|
-
*/
|
|
15
|
-
start: () => void;
|
|
16
|
-
/**
|
|
17
|
-
* Disconnect from the server and stop receiving messages.
|
|
18
|
-
*/
|
|
19
|
-
stop: () => void;
|
|
20
8
|
/**
|
|
21
9
|
* Register a handler for a message type.
|
|
10
|
+
*
|
|
11
|
+
* The first time this is called, the client will be considered ready.
|
|
12
|
+
*
|
|
13
|
+
* @param type The type of message to handle.
|
|
14
|
+
* @param handler The handler to call when a message of the given type is received.
|
|
15
|
+
* @returns A function to unregister the handler.
|
|
22
16
|
*/
|
|
23
|
-
onMessage:
|
|
24
|
-
|
|
25
|
-
handler: (message: M) => void,
|
|
26
|
-
) => void;
|
|
17
|
+
onMessage(type: string, handler: (message: any) => void): () => void;
|
|
18
|
+
|
|
27
19
|
/**
|
|
28
20
|
* Send a message to the server.
|
|
21
|
+
*
|
|
22
|
+
* @param message The message to send. Messages must have a `type` property.
|
|
29
23
|
*/
|
|
30
|
-
sendMessage: (message:
|
|
24
|
+
sendMessage: (message: any) => void;
|
|
25
|
+
|
|
31
26
|
/**
|
|
32
|
-
*
|
|
27
|
+
* Load a module from the server.
|
|
28
|
+
* @param moduleName The name of the module to load.
|
|
29
|
+
* @returns A promise that resolves to the module.
|
|
33
30
|
*/
|
|
34
31
|
loadModule: (moduleName: string) => Promise<ReactPyModule>;
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
export class BaseReactPyClient implements Partial<ReactPyClient> {
|
|
35
|
+
private readonly handlers: { [key: string]: ((message: any) => void)[] } = {};
|
|
36
|
+
protected readonly ready: Promise<void>;
|
|
37
|
+
private resolveReady: (value: undefined) => void;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.resolveReady = () => {};
|
|
41
|
+
this.ready = new Promise((resolve) => (this.resolveReady = resolve));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onMessage(type: string, handler: (message: any) => void): () => void {
|
|
45
|
+
(this.handlers[type] || (this.handlers[type] = [])).push(handler);
|
|
46
|
+
this.resolveReady(undefined);
|
|
47
|
+
return () => {
|
|
48
|
+
this.handlers[type] = this.handlers[type].filter((h) => h !== handler);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle an incoming message.
|
|
54
|
+
*
|
|
55
|
+
* This should be called by subclasses when a message is received.
|
|
56
|
+
*
|
|
57
|
+
* @param message The message to handle. The message must have a `type` property.
|
|
58
|
+
*/
|
|
59
|
+
protected handleIncoming(message: any): void {
|
|
60
|
+
if (!message.type) {
|
|
61
|
+
logger.warn("Received message without type", message);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const messageHandlers: ((m: any) => void)[] | undefined =
|
|
66
|
+
this.handlers[message.type];
|
|
67
|
+
if (!messageHandlers) {
|
|
68
|
+
logger.warn("Received message without handler", message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
messageHandlers.forEach((h) => h(message));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
export type SimpleReactPyClientProps = {
|
|
38
77
|
serverLocation?: LocationProps;
|
|
39
78
|
reconnectOptions?: ReconnectProps;
|
|
@@ -77,21 +116,15 @@ type ReconnectProps = {
|
|
|
77
116
|
intervalJitter?: number;
|
|
78
117
|
};
|
|
79
118
|
|
|
80
|
-
export class SimpleReactPyClient
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
export class SimpleReactPyClient
|
|
120
|
+
extends BaseReactPyClient
|
|
121
|
+
implements ReactPyClient
|
|
122
|
+
{
|
|
83
123
|
private readonly urls: ServerUrls;
|
|
84
|
-
private readonly handlers: {
|
|
85
|
-
[key in IncomingMessage["type"]]: ((message: any) => void)[];
|
|
86
|
-
};
|
|
87
124
|
private readonly socket: { current?: WebSocket };
|
|
88
125
|
|
|
89
126
|
constructor(props: SimpleReactPyClientProps) {
|
|
90
|
-
|
|
91
|
-
"connection-open": [],
|
|
92
|
-
"connection-close": [],
|
|
93
|
-
"layout-update": [],
|
|
94
|
-
};
|
|
127
|
+
super();
|
|
95
128
|
|
|
96
129
|
this.urls = getServerUrls(
|
|
97
130
|
props.serverLocation || {
|
|
@@ -101,66 +134,21 @@ export class SimpleReactPyClient implements ReactPyClient {
|
|
|
101
134
|
},
|
|
102
135
|
);
|
|
103
136
|
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
this.resolveShouldClose = () => {
|
|
108
|
-
throw new Error("Could not stop client");
|
|
109
|
-
};
|
|
110
|
-
const shouldOpen = new Promise((r) => (this.resolveShouldOpen = r));
|
|
111
|
-
const shouldClose = new Promise((r) => (this.resolveShouldClose = r));
|
|
112
|
-
|
|
113
|
-
this.socket = startReconnectingWebSocket({
|
|
114
|
-
shouldOpen,
|
|
115
|
-
shouldClose,
|
|
137
|
+
this.socket = createReconnectingWebSocket({
|
|
138
|
+
readyPromise: this.ready,
|
|
116
139
|
url: this.urls.stream,
|
|
117
|
-
onOpen: () => this.handleIncoming({ type: "connection-open" }),
|
|
118
140
|
onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)),
|
|
119
|
-
onClose: () => this.handleIncoming({ type: "connection-close" }),
|
|
120
141
|
...props.reconnectOptions,
|
|
121
142
|
});
|
|
122
143
|
}
|
|
123
144
|
|
|
124
|
-
|
|
125
|
-
logger.log("starting client...");
|
|
126
|
-
this.resolveShouldOpen(undefined);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
stop(): void {
|
|
130
|
-
logger.log("stopping client...");
|
|
131
|
-
this.resolveShouldClose(undefined);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
onMessage<M extends IncomingMessage>(
|
|
135
|
-
type: M["type"],
|
|
136
|
-
handler: (message: M) => void,
|
|
137
|
-
): void {
|
|
138
|
-
this.handlers[type].push(handler);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
sendMessage(message: OutgoingMessage): void {
|
|
145
|
+
sendMessage(message: any): void {
|
|
142
146
|
this.socket.current?.send(JSON.stringify(message));
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
loadModule(moduleName: string): Promise<ReactPyModule> {
|
|
146
150
|
return import(`${this.urls.modules}/${moduleName}`);
|
|
147
151
|
}
|
|
148
|
-
|
|
149
|
-
private handleIncoming(message: IncomingMessage): void {
|
|
150
|
-
if (!message.type) {
|
|
151
|
-
logger.warn("Received message without type", message);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const messageHandlers: ((m: any) => void)[] | undefined =
|
|
156
|
-
this.handlers[message.type];
|
|
157
|
-
if (!messageHandlers) {
|
|
158
|
-
logger.warn("Received message without handler", message);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
messageHandlers.forEach((h) => h(message));
|
|
163
|
-
}
|
|
164
152
|
}
|
|
165
153
|
|
|
166
154
|
type ServerUrls = {
|
|
@@ -182,14 +170,13 @@ function getServerUrls(props: LocationProps): ServerUrls {
|
|
|
182
170
|
return { base, modules, assets, stream };
|
|
183
171
|
}
|
|
184
172
|
|
|
185
|
-
function
|
|
173
|
+
function createReconnectingWebSocket(
|
|
186
174
|
props: {
|
|
187
|
-
shouldOpen: Promise<any>;
|
|
188
|
-
shouldClose: Promise<any>;
|
|
189
175
|
url: string;
|
|
190
|
-
|
|
176
|
+
readyPromise: Promise<void>;
|
|
177
|
+
onOpen?: () => void;
|
|
191
178
|
onMessage: (message: MessageEvent<any>) => void;
|
|
192
|
-
onClose
|
|
179
|
+
onClose?: () => void;
|
|
193
180
|
} & ReconnectProps,
|
|
194
181
|
) {
|
|
195
182
|
const {
|
|
@@ -202,7 +189,7 @@ function startReconnectingWebSocket(
|
|
|
202
189
|
const startInterval = 750;
|
|
203
190
|
let retries = 0;
|
|
204
191
|
let interval = startInterval;
|
|
205
|
-
|
|
192
|
+
const closed = false;
|
|
206
193
|
let everConnected = false;
|
|
207
194
|
const socket: { current?: WebSocket } = {};
|
|
208
195
|
|
|
@@ -216,7 +203,9 @@ function startReconnectingWebSocket(
|
|
|
216
203
|
logger.log("client connected");
|
|
217
204
|
interval = startInterval;
|
|
218
205
|
retries = 0;
|
|
219
|
-
props.onOpen
|
|
206
|
+
if (props.onOpen) {
|
|
207
|
+
props.onOpen();
|
|
208
|
+
}
|
|
220
209
|
};
|
|
221
210
|
socket.current.onmessage = props.onMessage;
|
|
222
211
|
socket.current.onclose = () => {
|
|
@@ -226,7 +215,9 @@ function startReconnectingWebSocket(
|
|
|
226
215
|
}
|
|
227
216
|
|
|
228
217
|
logger.log("client disconnected");
|
|
229
|
-
props.onClose
|
|
218
|
+
if (props.onClose) {
|
|
219
|
+
props.onClose();
|
|
220
|
+
}
|
|
230
221
|
|
|
231
222
|
if (retries >= maxRetries) {
|
|
232
223
|
return;
|
|
@@ -242,11 +233,7 @@ function startReconnectingWebSocket(
|
|
|
242
233
|
};
|
|
243
234
|
};
|
|
244
235
|
|
|
245
|
-
props.
|
|
246
|
-
props.shouldClose.then(() => {
|
|
247
|
-
closed = true;
|
|
248
|
-
socket.current?.close();
|
|
249
|
-
});
|
|
236
|
+
props.readyPromise.then(() => logger.log("starting client...")).then(connect);
|
|
250
237
|
|
|
251
238
|
return socket;
|
|
252
239
|
}
|
package/src/reactpy-vdom.tsx
CHANGED
|
@@ -155,8 +155,8 @@ function createEventHandler(
|
|
|
155
155
|
): [string, () => void] {
|
|
156
156
|
return [
|
|
157
157
|
name,
|
|
158
|
-
function () {
|
|
159
|
-
const data = Array.from(
|
|
158
|
+
function (...args: any[]) {
|
|
159
|
+
const data = Array.from(args).map((value) => {
|
|
160
160
|
if (!(typeof value === "object" && value.nativeEvent)) {
|
|
161
161
|
return value;
|
|
162
162
|
}
|