Skip to content

Commit ae242ba

Browse files
committed
rewrite @reactpy/client
1 parent a91ca99 commit ae242ba

File tree

10 files changed

+375
-371
lines changed

10 files changed

+375
-371
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import logger from "./logger";
2+
import {
3+
ReactPyClientInterface,
4+
ReactPyModule,
5+
GenericReactPyClientProps,
6+
ReactPyUrls,
7+
} from "./types";
8+
import { createReconnectingWebSocket } from "./websocket";
9+
10+
export abstract class BaseReactPyClient implements ReactPyClientInterface {
11+
private readonly handlers: { [key: string]: ((message: any) => void)[] } = {};
12+
protected readonly ready: Promise<void>;
13+
private resolveReady: (value: undefined) => void;
14+
15+
constructor() {
16+
this.resolveReady = () => {};
17+
this.ready = new Promise((resolve) => (this.resolveReady = resolve));
18+
}
19+
20+
onMessage(type: string, handler: (message: any) => void): () => void {
21+
(this.handlers[type] || (this.handlers[type] = [])).push(handler);
22+
this.resolveReady(undefined);
23+
return () => {
24+
this.handlers[type] = this.handlers[type].filter((h) => h !== handler);
25+
};
26+
}
27+
28+
abstract sendMessage(message: any): void;
29+
abstract loadModule(moduleName: string): Promise<ReactPyModule>;
30+
31+
/**
32+
* Handle an incoming message.
33+
*
34+
* This should be called by subclasses when a message is received.
35+
*
36+
* @param message The message to handle. The message must have a `type` property.
37+
*/
38+
protected handleIncoming(message: any): void {
39+
if (!message.type) {
40+
logger.warn("Received message without type", message);
41+
return;
42+
}
43+
44+
const messageHandlers: ((m: any) => void)[] | undefined =
45+
this.handlers[message.type];
46+
if (!messageHandlers) {
47+
logger.warn("Received message without handler", message);
48+
return;
49+
}
50+
51+
messageHandlers.forEach((h) => h(message));
52+
}
53+
}
54+
55+
export class ReactPyClient
56+
extends BaseReactPyClient
57+
implements ReactPyClientInterface
58+
{
59+
urls: ReactPyUrls;
60+
socket: { current?: WebSocket };
61+
mountElement: HTMLElement;
62+
63+
constructor(props: GenericReactPyClientProps) {
64+
super();
65+
66+
this.urls = props.urls;
67+
this.mountElement = props.mountElement;
68+
this.socket = createReconnectingWebSocket({
69+
url: this.urls.componentUrl,
70+
readyPromise: this.ready,
71+
...props.reconnectOptions,
72+
onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)),
73+
});
74+
}
75+
76+
sendMessage(message: any): void {
77+
this.socket.current?.send(JSON.stringify(message));
78+
}
79+
80+
loadModule(moduleName: string): Promise<ReactPyModule> {
81+
return import(`${this.urls.jsModules}/${moduleName}`);
82+
}
83+
}

src/js/packages/@reactpy/client/src/components.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
1+
import { set as setJsonPointer } from "json-pointer";
12
import React, {
2-
createElement,
3+
ChangeEvent,
34
createContext,
4-
useState,
5-
useRef,
6-
useContext,
7-
useEffect,
5+
createElement,
86
Fragment,
97
MutableRefObject,
10-
ChangeEvent,
8+
useContext,
9+
useEffect,
10+
useRef,
11+
useState,
1112
} from "preact/compat";
12-
// @ts-ignore
13-
import { set as setJsonPointer } from "json-pointer";
1413
import {
15-
ReactPyVdom,
16-
ReactPyComponent,
17-
createChildren,
18-
createAttributes,
19-
loadImportSource,
2014
ImportSourceBinding,
21-
} from "./reactpy-vdom";
22-
import { ReactPyClient } from "./reactpy-client";
15+
ReactPyComponent,
16+
ReactPyVdom,
17+
ReactPyClientInterface,
18+
} from "./types";
19+
import { createAttributes, createChildren, loadImportSource } from "./vdom";
2320

24-
const ClientContext = createContext<ReactPyClient>(null as any);
21+
const ClientContext = createContext<ReactPyClientInterface>(null as any);
2522

26-
export function Layout(props: { client: ReactPyClient }): JSX.Element {
23+
export function Layout(props: { client: ReactPyClientInterface }): JSX.Element {
2724
const currentModel: ReactPyVdom = useState({ tagName: "" })[0];
2825
const forceUpdate = useForceUpdate();
2926

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
export * from "./client";
12
export * from "./components";
2-
export * from "./messages";
33
export * from "./mount";
4-
export * from "./reactpy-client";
5-
export * from "./reactpy-vdom";
4+
export * from "./types";
5+
export * from "./vdom";
6+
export * from "./websocket";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export default {
22
log: (...args: any[]): void => console.log("[ReactPy]", ...args),
3+
info: (...args: any[]): void => console.info("[ReactPy]", ...args),
34
warn: (...args: any[]): void => console.warn("[ReactPy]", ...args),
45
error: (...args: any[]): void => console.error("[ReactPy]", ...args),
56
};

src/js/packages/@reactpy/client/src/messages.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,37 @@
1-
import React from "preact/compat";
2-
import { render } from "preact/compat";
1+
import { default as React, default as ReactDOM } from "preact/compat";
2+
import { ReactPyClient } from "./client";
33
import { Layout } from "./components";
4-
import { ReactPyClient } from "./reactpy-client";
4+
import { MountProps } from "./types";
55

6-
export function mount(element: HTMLElement, client: ReactPyClient): void {
7-
render(<Layout client={client} />, element);
6+
export function mount(props: MountProps) {
7+
// WebSocket route for component rendering
8+
const wsProtocol = `ws${window.location.protocol === "https:" ? "s" : ""}:`;
9+
let wsOrigin = `${wsProtocol}//${window.location.host}`;
10+
let componentUrl = new URL(`${wsOrigin}/${props.componentPath}`);
11+
12+
// Embed the initial HTTP path into the WebSocket URL
13+
componentUrl.searchParams.append("http_pathname", window.location.pathname);
14+
if (window.location.search) {
15+
componentUrl.searchParams.append("http_search", window.location.search);
16+
}
17+
18+
// Configure a new ReactPy client
19+
const client = new ReactPyClient({
20+
urls: {
21+
componentUrl: componentUrl,
22+
query: document.location.search,
23+
jsModules: `${window.location.origin}/${props.jsModulesPath}`,
24+
},
25+
reconnectOptions: {
26+
startInterval: props.reconnectStartInterval || 750,
27+
maxInterval: props.reconnectMaxInterval || 60000,
28+
backoffMultiplier: props.reconnectBackoffMultiplier || 1.25,
29+
maxRetries: props.reconnectMaxRetries || 150,
30+
},
31+
mountElement: props.mountElement,
32+
});
33+
34+
// Start rendering the component
35+
// eslint-disable-next-line react/no-deprecated
36+
ReactDOM.render(<Layout client={client} />, props.mountElement);
837
}

0 commit comments

Comments
 (0)