@reactpy/client 0.1.0 → 0.2.1
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 +10 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +172 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +5 -0
- package/dist/messages.d.ts +24 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +1 -0
- package/dist/mount.d.ts +3 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +6 -0
- package/dist/reactpy-client.d.ts +86 -0
- package/dist/reactpy-client.d.ts.map +1 -0
- package/dist/reactpy-client.js +133 -0
- package/dist/reactpy-vdom.d.ts +54 -0
- package/dist/reactpy-vdom.d.ts.map +1 -0
- package/dist/reactpy-vdom.js +140 -0
- package/package.json +22 -20
- package/src/components.tsx +231 -0
- package/src/index.ts +5 -0
- package/src/logger.ts +5 -0
- package/src/messages.ts +32 -0
- package/src/mount.tsx +8 -0
- package/src/reactpy-client.ts +274 -0
- package/src/reactpy-vdom.tsx +261 -0
- package/tsconfig.json +14 -0
- package/src/components.js +0 -220
- package/src/contexts.js +0 -6
- package/src/element-utils.js +0 -82
- package/src/event-to-object.js +0 -240
- package/src/import-source.js +0 -134
- package/src/index.js +0 -4
- package/src/mount.js +0 -105
- package/src/server.js +0 -46
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { ReactPyVdom } from "./reactpy-vdom";
|
|
3
|
+
import { ReactPyClient } from "./reactpy-client";
|
|
4
|
+
export declare function Layout(props: {
|
|
5
|
+
client: ReactPyClient;
|
|
6
|
+
}): JSX.Element;
|
|
7
|
+
export declare function Element({ model }: {
|
|
8
|
+
model: ReactPyVdom;
|
|
9
|
+
}): JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":";AAcA,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,CAyBpE;AAED,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAoB7E"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { createElement, createContext, useState, useRef, useContext, useEffect, Fragment, } from "react";
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { set as setJsonPointer } from "json-pointer";
|
|
4
|
+
import { createChildren, createAttributes, loadImportSource, } from "./reactpy-vdom";
|
|
5
|
+
const ClientContext = createContext(null);
|
|
6
|
+
export function Layout(props) {
|
|
7
|
+
const currentModel = useState({ tagName: "" })[0];
|
|
8
|
+
const forceUpdate = useForceUpdate();
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
props.client.onMessage("layout-update", ({ path, model }) => {
|
|
11
|
+
if (path === "") {
|
|
12
|
+
Object.assign(currentModel, model);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
setJsonPointer(currentModel, path, model);
|
|
16
|
+
}
|
|
17
|
+
forceUpdate();
|
|
18
|
+
});
|
|
19
|
+
props.client.start();
|
|
20
|
+
return () => props.client.stop();
|
|
21
|
+
}, [currentModel, props.client]);
|
|
22
|
+
return (React.createElement(ClientContext.Provider, { value: props.client },
|
|
23
|
+
React.createElement(Element, { model: currentModel })));
|
|
24
|
+
}
|
|
25
|
+
export function Element({ model }) {
|
|
26
|
+
if (model.error !== undefined) {
|
|
27
|
+
if (model.error) {
|
|
28
|
+
return React.createElement("pre", null, model.error);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let SpecializedElement;
|
|
35
|
+
if (model.tagName in SPECIAL_ELEMENTS) {
|
|
36
|
+
SpecializedElement =
|
|
37
|
+
SPECIAL_ELEMENTS[model.tagName];
|
|
38
|
+
}
|
|
39
|
+
else if (model.importSource) {
|
|
40
|
+
SpecializedElement = ImportedElement;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
SpecializedElement = StandardElement;
|
|
44
|
+
}
|
|
45
|
+
return React.createElement(SpecializedElement, { model: model });
|
|
46
|
+
}
|
|
47
|
+
function StandardElement({ model }) {
|
|
48
|
+
const client = React.useContext(ClientContext);
|
|
49
|
+
// Use createElement here to avoid warning about variable numbers of children not
|
|
50
|
+
// having keys. Warning about this must now be the responsibility of the client
|
|
51
|
+
// providing the models instead of the client rendering them.
|
|
52
|
+
return createElement(model.tagName === "" ? Fragment : model.tagName, createAttributes(model, client), ...createChildren(model, (child) => {
|
|
53
|
+
return React.createElement(Element, { model: child, key: child.key });
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function UserInputElement({ model }) {
|
|
57
|
+
const client = useContext(ClientContext);
|
|
58
|
+
const props = createAttributes(model, client);
|
|
59
|
+
const [value, setValue] = React.useState(props.value);
|
|
60
|
+
// honor changes to value from the client via props
|
|
61
|
+
React.useEffect(() => setValue(props.value), [props.value]);
|
|
62
|
+
const givenOnChange = props.onChange;
|
|
63
|
+
if (typeof givenOnChange === "function") {
|
|
64
|
+
props.onChange = (event) => {
|
|
65
|
+
// immediately update the value to give the user feedback
|
|
66
|
+
setValue(event.target.value);
|
|
67
|
+
// allow the client to respond (and possibly change the value)
|
|
68
|
+
givenOnChange(event);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Use createElement here to avoid warning about variable numbers of children not
|
|
72
|
+
// having keys. Warning about this must now be the responsibility of the client
|
|
73
|
+
// providing the models instead of the client rendering them.
|
|
74
|
+
return createElement(model.tagName,
|
|
75
|
+
// overwrite
|
|
76
|
+
{ ...props, value }, ...createChildren(model, (child) => (React.createElement(Element, { model: child, key: child.key }))));
|
|
77
|
+
}
|
|
78
|
+
function ScriptElement({ model }) {
|
|
79
|
+
const ref = useRef(null);
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (!ref.current) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const scriptContent = model?.children?.filter((value) => typeof value == "string")[0];
|
|
85
|
+
let scriptElement;
|
|
86
|
+
if (model.attributes) {
|
|
87
|
+
scriptElement = document.createElement("script");
|
|
88
|
+
for (const [k, v] of Object.entries(model.attributes)) {
|
|
89
|
+
scriptElement.setAttribute(k, v);
|
|
90
|
+
}
|
|
91
|
+
if (scriptContent) {
|
|
92
|
+
scriptElement.appendChild(document.createTextNode(scriptContent));
|
|
93
|
+
}
|
|
94
|
+
ref.current.appendChild(scriptElement);
|
|
95
|
+
}
|
|
96
|
+
else if (scriptContent) {
|
|
97
|
+
let scriptResult = eval(scriptContent);
|
|
98
|
+
if (typeof scriptResult == "function") {
|
|
99
|
+
return scriptResult();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [model.key, ref.current]);
|
|
103
|
+
return React.createElement("div", { ref: ref });
|
|
104
|
+
}
|
|
105
|
+
function ImportedElement({ model }) {
|
|
106
|
+
const importSourceVdom = model.importSource;
|
|
107
|
+
const importSourceRef = useImportSource(model);
|
|
108
|
+
if (!importSourceVdom) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const importSourceFallback = importSourceVdom.fallback;
|
|
112
|
+
if (!importSourceVdom) {
|
|
113
|
+
// display a fallback if one was given
|
|
114
|
+
if (!importSourceFallback) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
else if (typeof importSourceFallback === "string") {
|
|
118
|
+
return React.createElement("div", null, importSourceFallback);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
return React.createElement(StandardElement, { model: importSourceFallback });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return React.createElement("div", { ref: importSourceRef });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function useForceUpdate() {
|
|
129
|
+
const [, setState] = useState(false);
|
|
130
|
+
return () => setState((old) => !old);
|
|
131
|
+
}
|
|
132
|
+
function useImportSource(model) {
|
|
133
|
+
const vdomImportSource = model.importSource;
|
|
134
|
+
const mountPoint = useRef(null);
|
|
135
|
+
const client = React.useContext(ClientContext);
|
|
136
|
+
const [binding, setBinding] = useState(null);
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
let unmounted = false;
|
|
139
|
+
if (vdomImportSource) {
|
|
140
|
+
loadImportSource(vdomImportSource, client).then((bind) => {
|
|
141
|
+
if (!unmounted && mountPoint.current) {
|
|
142
|
+
setBinding(bind(mountPoint.current));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return () => {
|
|
147
|
+
unmounted = true;
|
|
148
|
+
if (binding &&
|
|
149
|
+
vdomImportSource &&
|
|
150
|
+
!vdomImportSource.unmountBeforeUpdate) {
|
|
151
|
+
binding.unmount();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}, [client, vdomImportSource, setBinding, mountPoint.current]);
|
|
155
|
+
// this effect must run every time in case the model has changed
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!(binding && vdomImportSource)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
binding.render(model);
|
|
161
|
+
if (vdomImportSource.unmountBeforeUpdate) {
|
|
162
|
+
return binding.unmount;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return mountPoint;
|
|
166
|
+
}
|
|
167
|
+
const SPECIAL_ELEMENTS = {
|
|
168
|
+
input: UserInputElement,
|
|
169
|
+
script: ScriptElement,
|
|
170
|
+
select: UserInputElement,
|
|
171
|
+
textarea: UserInputElement,
|
|
172
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":";mBACiB,GAAG,EAAE,KAAG,IAAI;oBACX,GAAG,EAAE,KAAG,IAAI;qBACX,GAAG,EAAE,KAAG,IAAI;;AAH/B,wBAIE"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export type LayoutUpdateMessage = {
|
|
12
|
+
type: "layout-update";
|
|
13
|
+
path: string;
|
|
14
|
+
model: ReactPyVdom;
|
|
15
|
+
};
|
|
16
|
+
export type LayoutEventMessage = {
|
|
17
|
+
type: "layout-event";
|
|
18
|
+
target: string;
|
|
19
|
+
data: any;
|
|
20
|
+
};
|
|
21
|
+
export type IncomingMessage = LayoutUpdateMessage | ConnectionOpenMessage | ConnectionCloseMessage;
|
|
22
|
+
export type OutgoingMessage = LayoutEventMessage;
|
|
23
|
+
export type Message = IncomingMessage | OutgoingMessage;
|
|
24
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +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,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,iBAAiB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,kBAAkB,CAAC;CAC1B,CAAC;AAEF,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,GACvB,mBAAmB,GACnB,qBAAqB,GACrB,sBAAsB,CAAC;AAC3B,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AACjD,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,eAAe,CAAC"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,wBAAgB,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAEvE"}
|
package/dist/mount.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { OutgoingMessage, IncomingMessage } from "./messages";
|
|
2
|
+
import { ReactPyModule } from "./reactpy-vdom";
|
|
3
|
+
/**
|
|
4
|
+
* A client for communicating with a ReactPy server.
|
|
5
|
+
*/
|
|
6
|
+
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
|
+
/**
|
|
19
|
+
* Register a handler for a message type.
|
|
20
|
+
*/
|
|
21
|
+
onMessage: <M extends IncomingMessage>(type: M["type"], handler: (message: M) => void) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Send a message to the server.
|
|
24
|
+
*/
|
|
25
|
+
sendMessage: (message: OutgoingMessage) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Dynamically load a module from the server.
|
|
28
|
+
*/
|
|
29
|
+
loadModule: (moduleName: string) => Promise<ReactPyModule>;
|
|
30
|
+
}
|
|
31
|
+
export type SimpleReactPyClientProps = {
|
|
32
|
+
serverLocation?: LocationProps;
|
|
33
|
+
reconnectOptions?: ReconnectProps;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The location of the server.
|
|
37
|
+
*
|
|
38
|
+
* This is used to determine the location of the server's API endpoints. All endpoints
|
|
39
|
+
* are expected to be found at the base URL, with the following paths:
|
|
40
|
+
*
|
|
41
|
+
* - `_reactpy/stream/${route}${query}`: The websocket endpoint for the stream.
|
|
42
|
+
* - `_reactpy/modules`: The directory containing the dynamically loaded modules.
|
|
43
|
+
* - `_reactpy/assets`: The directory containing the static assets.
|
|
44
|
+
*/
|
|
45
|
+
type LocationProps = {
|
|
46
|
+
/**
|
|
47
|
+
* The base URL of the server.
|
|
48
|
+
*
|
|
49
|
+
* @default - document.location.origin
|
|
50
|
+
*/
|
|
51
|
+
url: string;
|
|
52
|
+
/**
|
|
53
|
+
* The route to the page being rendered.
|
|
54
|
+
*
|
|
55
|
+
* @default - document.location.pathname
|
|
56
|
+
*/
|
|
57
|
+
route: string;
|
|
58
|
+
/**
|
|
59
|
+
* The query string of the page being rendered.
|
|
60
|
+
*
|
|
61
|
+
* @default - document.location.search
|
|
62
|
+
*/
|
|
63
|
+
query: string;
|
|
64
|
+
};
|
|
65
|
+
type ReconnectProps = {
|
|
66
|
+
maxInterval?: number;
|
|
67
|
+
maxRetries?: number;
|
|
68
|
+
backoffRate?: number;
|
|
69
|
+
intervalJitter?: number;
|
|
70
|
+
};
|
|
71
|
+
export declare class SimpleReactPyClient implements ReactPyClient {
|
|
72
|
+
private resolveShouldOpen;
|
|
73
|
+
private resolveShouldClose;
|
|
74
|
+
private readonly urls;
|
|
75
|
+
private readonly handlers;
|
|
76
|
+
private readonly socket;
|
|
77
|
+
constructor(props: SimpleReactPyClientProps);
|
|
78
|
+
start(): void;
|
|
79
|
+
stop(): void;
|
|
80
|
+
onMessage<M extends IncomingMessage>(type: M["type"], handler: (message: M) => void): void;
|
|
81
|
+
sendMessage(message: OutgoingMessage): void;
|
|
82
|
+
loadModule(moduleName: string): Promise<ReactPyModule>;
|
|
83
|
+
private handleIncoming;
|
|
84
|
+
}
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=reactpy-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactpy-client.d.ts","sourceRoot":"","sources":["../src/reactpy-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB;;OAEG;IACH,SAAS,EAAE,CAAC,CAAC,SAAS,eAAe,EACnC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EACf,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,KAC1B,IAAI,CAAC;IACV;;OAEG;IACH,WAAW,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD;;OAEG;IACH,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5D;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,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAEvB;IACF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBAErC,KAAK,EAAE,wBAAwB;IAmC3C,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAKZ,SAAS,CAAC,CAAC,SAAS,eAAe,EACjC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EACf,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAC5B,IAAI;IAIP,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAI3C,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAItD,OAAO,CAAC,cAAc;CAevB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import logger from "./logger";
|
|
2
|
+
export class SimpleReactPyClient {
|
|
3
|
+
resolveShouldOpen;
|
|
4
|
+
resolveShouldClose;
|
|
5
|
+
urls;
|
|
6
|
+
handlers;
|
|
7
|
+
socket;
|
|
8
|
+
constructor(props) {
|
|
9
|
+
this.handlers = {
|
|
10
|
+
"connection-open": [],
|
|
11
|
+
"connection-close": [],
|
|
12
|
+
"layout-update": [],
|
|
13
|
+
};
|
|
14
|
+
this.urls = getServerUrls(props.serverLocation || {
|
|
15
|
+
url: document.location.origin,
|
|
16
|
+
route: document.location.pathname,
|
|
17
|
+
query: document.location.search,
|
|
18
|
+
});
|
|
19
|
+
this.resolveShouldOpen = () => {
|
|
20
|
+
throw new Error("Could not start client");
|
|
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,
|
|
30
|
+
url: this.urls.stream,
|
|
31
|
+
onOpen: () => this.handleIncoming({ type: "connection-open" }),
|
|
32
|
+
onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)),
|
|
33
|
+
onClose: () => this.handleIncoming({ type: "connection-close" }),
|
|
34
|
+
...props.reconnectOptions,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
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
|
+
sendMessage(message) {
|
|
49
|
+
this.socket.current?.send(JSON.stringify(message));
|
|
50
|
+
}
|
|
51
|
+
loadModule(moduleName) {
|
|
52
|
+
return import(`${this.urls.modules}/${moduleName}`);
|
|
53
|
+
}
|
|
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
|
+
}
|
|
67
|
+
function getServerUrls(props) {
|
|
68
|
+
const base = new URL(`${props.url || document.location.origin}/_reactpy`);
|
|
69
|
+
const modules = `${base}/modules`;
|
|
70
|
+
const assets = `${base}/assets`;
|
|
71
|
+
const streamProtocol = `ws${base.protocol === "https:" ? "s" : ""}`;
|
|
72
|
+
const streamPath = rtrim(`${base.pathname}/stream${props.route || ""}`, "/");
|
|
73
|
+
const stream = `${streamProtocol}://${base.host}${streamPath}${props.query}`;
|
|
74
|
+
return { base, modules, assets, stream };
|
|
75
|
+
}
|
|
76
|
+
function startReconnectingWebSocket(props) {
|
|
77
|
+
const { maxInterval = 60000, maxRetries = 50, backoffRate = 1.1, intervalJitter = 0.1, } = props;
|
|
78
|
+
const startInterval = 750;
|
|
79
|
+
let retries = 0;
|
|
80
|
+
let interval = startInterval;
|
|
81
|
+
let closed = false;
|
|
82
|
+
let everConnected = false;
|
|
83
|
+
const socket = {};
|
|
84
|
+
const connect = () => {
|
|
85
|
+
if (closed) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
socket.current = new WebSocket(props.url);
|
|
89
|
+
socket.current.onopen = () => {
|
|
90
|
+
everConnected = true;
|
|
91
|
+
logger.log("client connected");
|
|
92
|
+
interval = startInterval;
|
|
93
|
+
retries = 0;
|
|
94
|
+
props.onOpen();
|
|
95
|
+
};
|
|
96
|
+
socket.current.onmessage = props.onMessage;
|
|
97
|
+
socket.current.onclose = () => {
|
|
98
|
+
if (!everConnected) {
|
|
99
|
+
logger.log("failed to connect");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
logger.log("client disconnected");
|
|
103
|
+
props.onClose();
|
|
104
|
+
if (retries >= maxRetries) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const thisInterval = addJitter(interval, intervalJitter);
|
|
108
|
+
logger.log(`reconnecting in ${(thisInterval / 1000).toPrecision(4)} seconds...`);
|
|
109
|
+
setTimeout(connect, thisInterval);
|
|
110
|
+
interval = nextInterval(interval, backoffRate, maxInterval);
|
|
111
|
+
retries++;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
props.shouldOpen.then(connect);
|
|
115
|
+
props.shouldClose.then(() => {
|
|
116
|
+
closed = true;
|
|
117
|
+
socket.current?.close();
|
|
118
|
+
});
|
|
119
|
+
return socket;
|
|
120
|
+
}
|
|
121
|
+
function nextInterval(currentInterval, backoffRate, maxInterval) {
|
|
122
|
+
return Math.min(currentInterval *
|
|
123
|
+
// increase interval by backoff rate
|
|
124
|
+
backoffRate,
|
|
125
|
+
// don't exceed max interval
|
|
126
|
+
maxInterval);
|
|
127
|
+
}
|
|
128
|
+
function addJitter(interval, jitter) {
|
|
129
|
+
return interval + (Math.random() * jitter * interval * 2 - jitter * interval);
|
|
130
|
+
}
|
|
131
|
+
function rtrim(text, trim) {
|
|
132
|
+
return text.replace(new RegExp(`${trim}+$`), "");
|
|
133
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
import { ReactPyClient } from "./reactpy-client";
|
|
3
|
+
export declare function loadImportSource(vdomImportSource: ReactPyVdomImportSource, client: ReactPyClient): Promise<BindImportSource>;
|
|
4
|
+
export declare function createChildren<Child>(model: ReactPyVdom, createChild: (child: ReactPyVdom) => Child): (Child | string)[];
|
|
5
|
+
export declare function createAttributes(model: ReactPyVdom, client: ReactPyClient): {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
};
|
|
8
|
+
export type ReactPyComponent = ComponentType<{
|
|
9
|
+
model: ReactPyVdom;
|
|
10
|
+
}>;
|
|
11
|
+
export type ReactPyVdom = {
|
|
12
|
+
tagName: string;
|
|
13
|
+
key?: string;
|
|
14
|
+
attributes?: {
|
|
15
|
+
[key: string]: string;
|
|
16
|
+
};
|
|
17
|
+
children?: (ReactPyVdom | string)[];
|
|
18
|
+
error?: string;
|
|
19
|
+
eventHandlers?: {
|
|
20
|
+
[key: string]: ReactPyVdomEventHandler;
|
|
21
|
+
};
|
|
22
|
+
importSource?: ReactPyVdomImportSource;
|
|
23
|
+
};
|
|
24
|
+
export type ReactPyVdomEventHandler = {
|
|
25
|
+
target: string;
|
|
26
|
+
preventDefault?: boolean;
|
|
27
|
+
stopPropagation?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type ReactPyVdomImportSource = {
|
|
30
|
+
source: string;
|
|
31
|
+
sourceType?: "URL" | "NAME";
|
|
32
|
+
fallback?: string | ReactPyVdom;
|
|
33
|
+
unmountBeforeUpdate?: boolean;
|
|
34
|
+
};
|
|
35
|
+
export type ReactPyModule = {
|
|
36
|
+
bind: (node: HTMLElement, context: ReactPyModuleBindingContext) => ReactPyModuleBinding;
|
|
37
|
+
} & {
|
|
38
|
+
[key: string]: any;
|
|
39
|
+
};
|
|
40
|
+
export type ReactPyModuleBindingContext = {
|
|
41
|
+
sendMessage: ReactPyClient["sendMessage"];
|
|
42
|
+
onMessage: ReactPyClient["onMessage"];
|
|
43
|
+
};
|
|
44
|
+
export type ReactPyModuleBinding = {
|
|
45
|
+
create: (type: any, props?: any, children?: (any | string | ReactPyVdom)[]) => any;
|
|
46
|
+
render: (element: any) => void;
|
|
47
|
+
unmount: () => void;
|
|
48
|
+
};
|
|
49
|
+
export type BindImportSource = (node: HTMLElement) => ImportSourceBinding | null;
|
|
50
|
+
export type ImportSourceBinding = {
|
|
51
|
+
render: (model: ReactPyVdom) => void;
|
|
52
|
+
unmount: () => void;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=reactpy-vdom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactpy-vdom.d.ts","sourceRoot":"","sources":["../src/reactpy-vdom.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,uBAAuB,EACzC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,gBAAgB,CAAC,CA2C3B;AA+DD,wBAAgB,cAAc,CAAC,KAAK,EAClC,KAAK,EAAE,WAAW,EAClB,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,KAAK,GACzC,CAAC,KAAK,GAAG,MAAM,CAAC,EAAE,CAapB;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,aAAa,GACpB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAcxB;AA0DD,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AAErE,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,CAAC,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,uBAAuB,CAAA;KAAE,CAAC;IAC3D,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,CACJ,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,2BAA2B,KACjC,oBAAoB,CAAC;CAC3B,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAC;AAE3B,MAAM,MAAM,2BAA2B,GAAG;IACxC,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC1C,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,CACN,IAAI,EAAE,GAAG,EACT,KAAK,CAAC,EAAE,GAAG,EACX,QAAQ,CAAC,EAAE,CAAC,GAAG,GAAG,MAAM,GAAG,WAAW,CAAC,EAAE,KACtC,GAAG,CAAC;IACT,MAAM,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,WAAW,KACd,mBAAmB,GAAG,IAAI,CAAC;AAEhC,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC"}
|