From 0d6684c09980bdf9c3e880c2f6d9937d8bfda79b Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 17:24:29 -0700
Subject: [PATCH 01/16] functional new client code
---
docs/src/features/settings.md | 2 +-
requirements/pkg-deps.txt | 2 +-
src/js/package-lock.json | 189 +++++++++++-------
src/js/package.json | 12 +-
src/js/rollup.config.mjs | 4 +-
src/js/src/index.js | 59 ------
src/js/src/index.ts | 88 ++++++++
src/js/src/types.ts | 17 ++
src/js/src/utils.ts | 100 +++++++++
src/js/tsconfig.json | 7 +
src/reactpy_django/checks.py | 8 +-
src/reactpy_django/config.py | 31 ++-
...05_alter_componentsession_last_accessed.py | 17 ++
src/reactpy_django/models.py | 2 +-
.../templates/reactpy/component.html | 9 +-
src/reactpy_django/templatetags/reactpy.py | 7 +-
src/reactpy_django/utils.py | 6 +-
src/reactpy_django/websocket/consumer.py | 33 ++-
tests/test_app/tests/test_database.py | 4 +-
19 files changed, 432 insertions(+), 165 deletions(-)
delete mode 100644 src/js/src/index.js
create mode 100644 src/js/src/index.ts
create mode 100644 src/js/src/types.ts
create mode 100644 src/js/src/utils.ts
create mode 100644 src/js/tsconfig.json
create mode 100644 src/reactpy_django/migrations/0005_alter_componentsession_last_accessed.py
diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md
index 29ca81ad..a68ad25c 100644
--- a/docs/src/features/settings.md
+++ b/docs/src/features/settings.md
@@ -14,7 +14,7 @@ These are ReactPy-Django's default settings values. You can modify these values
| --- | --- | --- | --- |
| `REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache.
We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
| `REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database ReactPy uses to store session data. ReactPy requires a multiprocessing-safe and thread-safe database.
If configuring `REACTPY_DATABASE`, it is mandatory to also configure `DATABASE_ROUTERS` like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
-| `REACTPY_RECONNECT_MAX` | `#!python 259200` | `#!python 96000`, `#!python 60`, `#!python 0` | Maximum seconds between reconnection attempts before giving up.
Use `#!python 0` to prevent reconnection. |
+| `REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 96000`, `#!python 60`, `#!python 0` | Maximum seconds between reconnection attempts before giving up.
Use `#!python 0` to prevent reconnection. |
| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. |
| `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. |
| `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `AuthMiddlewareStack` and...
2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `backend` attribute. |
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index ee7156c3..8eecf7bc 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,5 +1,5 @@
channels >=4.0.0
-django >=4.1.0
+django >=4.2.0
reactpy >=1.0.2, <1.1.0
aiofile >=3.0
dill >=0.3.5
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index 024084a6..84321d16 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -5,14 +5,16 @@
"packages": {
"": {
"dependencies": {
- "@reactpy/client": "^0.1.0"
+ "@reactpy/client": "^0.3.1",
+ "@rollup/plugin-typescript": "^11.1.2",
+ "tslib": "^2.6.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.2",
- "prettier": "^2.8.3",
- "rollup": "^3.12.0"
+ "prettier": "^3.0.2",
+ "rollup": "^3.28.1"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
@@ -22,16 +24,16 @@
"dev": true
},
"node_modules/@reactpy/client": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.1.0.tgz",
- "integrity": "sha512-GVsP23Re29JAbLNOBJytcem8paNhLj+2SZ8n9GlnlHPWuV6chAofT0aGveepCj1I9DdeVfRjDL6hfTreJEaDdg==",
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz",
+ "integrity": "sha512-mvFwAvmRMgo7lTjkhkEJzBep6HX/wfm5BaNbtEMOUzto7G/h+z1AmqlOMXLH37DSI0iwfmCuNwy07EJM0JWZ0g==",
"dependencies": {
- "htm": "^3.0.3",
+ "event-to-object": "^0.1.2",
"json-pointer": "^0.6.2"
},
"peerDependencies": {
- "react": ">=16",
- "react-dom": ">=16"
+ "react": ">=16 <18",
+ "react-dom": ">=16 <18"
}
},
"node_modules/@rollup/plugin-commonjs": {
@@ -105,11 +107,35 @@
}
}
},
+ "node_modules/@rollup/plugin-typescript": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.2.tgz",
+ "integrity": "sha512-0ghSOCMcA7fl1JM+0gYRf+Q/HWyg+zg7/gDSc+fRLmlJWcW5K1I+CLRzaRhXf4Y3DRyPnnDo4M2ktw+a6JcDEg==",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.14.0||^3.0.0",
+ "tslib": "*",
+ "typescript": ">=3.7.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ },
+ "tslib": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/pluginutils": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
"integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
- "dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@@ -130,8 +156,7 @@
"node_modules/@types/estree": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
- "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
- "dev": true
+ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
},
"node_modules/@types/resolve": {
"version": "1.20.2",
@@ -184,8 +209,15 @@
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "dev": true
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
+ "node_modules/event-to-object": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/event-to-object/-/event-to-object-0.1.2.tgz",
+ "integrity": "sha512-+fUmp1XOCZiYomwe5Zxp4IlchuZZfdVdjFUk5MbgRT4M+V2TEWKc0jJwKLCX/nxlJ6xM5VUb/ylzERh7YDCRrg==",
+ "dependencies": {
+ "json-pointer": "^0.6.2"
+ }
},
"node_modules/foreach": {
"version": "2.0.6",
@@ -215,8 +247,7 @@
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/glob": {
"version": "8.1.0",
@@ -241,7 +272,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -249,11 +279,6 @@
"node": ">= 0.4.0"
}
},
- "node_modules/htm": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz",
- "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
- },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -289,7 +314,6 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
- "dev": true,
"dependencies": {
"has": "^1.0.3"
},
@@ -383,14 +407,12 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
"engines": {
"node": ">=8.6"
},
@@ -399,15 +421,15 @@
}
},
"node_modules/prettier": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
- "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz",
+ "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==",
"dev": true,
"bin": {
- "prettier": "bin-prettier.js"
+ "prettier": "bin/prettier.cjs"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
@@ -444,7 +466,6 @@
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
- "dev": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
@@ -458,10 +479,10 @@
}
},
"node_modules/rollup": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.0.tgz",
- "integrity": "sha512-4MZ8kA2HNYahIjz63rzrMMRvDqQDeS9LoriJvMuV0V6zIGysP36e9t4yObUfwdT9h/szXoHQideICftcdZklWg==",
- "dev": true,
+ "version": "3.28.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz",
+ "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==",
+ "devOptional": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -487,7 +508,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -495,6 +515,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
+ "node_modules/typescript": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -510,11 +548,11 @@
"dev": true
},
"@reactpy/client": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.1.0.tgz",
- "integrity": "sha512-GVsP23Re29JAbLNOBJytcem8paNhLj+2SZ8n9GlnlHPWuV6chAofT0aGveepCj1I9DdeVfRjDL6hfTreJEaDdg==",
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz",
+ "integrity": "sha512-mvFwAvmRMgo7lTjkhkEJzBep6HX/wfm5BaNbtEMOUzto7G/h+z1AmqlOMXLH37DSI0iwfmCuNwy07EJM0JWZ0g==",
"requires": {
- "htm": "^3.0.3",
+ "event-to-object": "^0.1.2",
"json-pointer": "^0.6.2"
}
},
@@ -556,11 +594,19 @@
"magic-string": "^0.27.0"
}
},
+ "@rollup/plugin-typescript": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.2.tgz",
+ "integrity": "sha512-0ghSOCMcA7fl1JM+0gYRf+Q/HWyg+zg7/gDSc+fRLmlJWcW5K1I+CLRzaRhXf4Y3DRyPnnDo4M2ktw+a6JcDEg==",
+ "requires": {
+ "@rollup/pluginutils": "^5.0.1",
+ "resolve": "^1.22.1"
+ }
+ },
"@rollup/pluginutils": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
"integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
- "dev": true,
"requires": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@@ -570,8 +616,7 @@
"@types/estree": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
- "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
- "dev": true
+ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ=="
},
"@types/resolve": {
"version": "1.20.2",
@@ -615,8 +660,15 @@
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "dev": true
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
+ "event-to-object": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/event-to-object/-/event-to-object-0.1.2.tgz",
+ "integrity": "sha512-+fUmp1XOCZiYomwe5Zxp4IlchuZZfdVdjFUk5MbgRT4M+V2TEWKc0jJwKLCX/nxlJ6xM5VUb/ylzERh7YDCRrg==",
+ "requires": {
+ "json-pointer": "^0.6.2"
+ }
},
"foreach": {
"version": "2.0.6",
@@ -639,8 +691,7 @@
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"glob": {
"version": "8.1.0",
@@ -659,16 +710,10 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
- "htm": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz",
- "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
- },
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -698,7 +743,6 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
- "dev": true,
"requires": {
"has": "^1.0.3"
}
@@ -777,19 +821,17 @@
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"prettier": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
- "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz",
+ "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==",
"dev": true
},
"react": {
@@ -817,7 +859,6 @@
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
- "dev": true,
"requires": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
@@ -825,10 +866,10 @@
}
},
"rollup": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.0.tgz",
- "integrity": "sha512-4MZ8kA2HNYahIjz63rzrMMRvDqQDeS9LoriJvMuV0V6zIGysP36e9t4yObUfwdT9h/szXoHQideICftcdZklWg==",
- "dev": true,
+ "version": "3.28.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz",
+ "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==",
+ "devOptional": true,
"requires": {
"fsevents": "~2.3.2"
}
@@ -846,8 +887,18 @@
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
+ },
+ "tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
+ "typescript": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "peer": true
},
"wrappy": {
"version": "1.0.2",
diff --git a/src/js/package.json b/src/js/package.json
index 92890671..40596a0d 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -1,6 +1,6 @@
{
"description": "reactpy-django client",
- "main": "src/index.js",
+ "main": "src/index.ts",
"type": "module",
"files": [
"src/**/*.js"
@@ -10,13 +10,15 @@
"format": "prettier --ignore-path .gitignore --write ."
},
"devDependencies": {
- "prettier": "^2.8.3",
- "rollup": "^3.12.0",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
- "@rollup/plugin-replace": "^5.0.2"
+ "@rollup/plugin-replace": "^5.0.2",
+ "prettier": "^3.0.2",
+ "rollup": "^3.28.1"
},
"dependencies": {
- "@reactpy/client": "^0.1.0"
+ "@reactpy/client": "^0.3.1",
+ "@rollup/plugin-typescript": "^11.1.2",
+ "tslib": "^2.6.2"
}
}
diff --git a/src/js/rollup.config.mjs b/src/js/rollup.config.mjs
index 50fbc637..79f93839 100644
--- a/src/js/rollup.config.mjs
+++ b/src/js/rollup.config.mjs
@@ -1,9 +1,10 @@
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import replace from "@rollup/plugin-replace";
+import typescript from "@rollup/plugin-typescript";
export default {
- input: "src/index.js",
+ input: "src/index.ts",
output: {
file: "../reactpy_django/static/reactpy_django/client.js",
format: "esm",
@@ -14,6 +15,7 @@ export default {
replace({
"process.env.NODE_ENV": JSON.stringify("production"),
}),
+ typescript(),
],
onwarn: function (warning) {
console.warn(warning.message);
diff --git a/src/js/src/index.js b/src/js/src/index.js
deleted file mode 100644
index 2ee74e07..00000000
--- a/src/js/src/index.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { mountLayoutWithWebSocket } from "@reactpy/client";
-
-// Set up a websocket at the base endpoint
-let HTTP_PROTOCOL = window.location.protocol;
-let WS_PROTOCOL = "";
-if (HTTP_PROTOCOL == "https:") {
- WS_PROTOCOL = "wss:";
-} else {
- WS_PROTOCOL = "ws:";
-}
-
-export function mountViewToElement(
- mountElement,
- reactpyHost,
- reactpyUrlPrefix,
- reactpyReconnectMax,
- reactpyComponentPath,
- reactpyResolvedWebModulesPath
-) {
- // Determine the Websocket route
- let wsOrigin;
- if (reactpyHost) {
- wsOrigin = `${WS_PROTOCOL}//${reactpyHost}`;
- } else {
- wsOrigin = `${WS_PROTOCOL}//${window.location.host}`;
- }
- const websocketUrl = `${wsOrigin}/${reactpyUrlPrefix}/${reactpyComponentPath}`;
-
- // Determine the HTTP route
- let httpOrigin;
- let webModulesPath;
- if (reactpyHost) {
- httpOrigin = `${HTTP_PROTOCOL}//${reactpyHost}`;
- webModulesPath = `${reactpyUrlPrefix}/web_module`;
- } else {
- httpOrigin = `${HTTP_PROTOCOL}//${window.location.host}`;
- if (reactpyResolvedWebModulesPath) {
- webModulesPath = reactpyResolvedWebModulesPath;
- } else {
- webModulesPath = `${reactpyUrlPrefix}/web_module`;
- }
- }
- const webModuleUrl = `${httpOrigin}/${webModulesPath}`;
-
- // Function that loads the JavaScript web module, if needed
- const loadImportSource = (source, sourceType) => {
- return import(
- sourceType == "NAME" ? `${webModuleUrl}/${source}` : source
- );
- };
-
- // Start rendering the component
- mountLayoutWithWebSocket(
- mountElement,
- websocketUrl,
- loadImportSource,
- reactpyReconnectMax
- );
-}
diff --git a/src/js/src/index.ts b/src/js/src/index.ts
new file mode 100644
index 00000000..d19dbf79
--- /dev/null
+++ b/src/js/src/index.ts
@@ -0,0 +1,88 @@
+import { mount, BaseReactPyClient, ReactPyClient } from "@reactpy/client";
+import { createReconnectingWebSocket } from "./utils";
+import { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
+
+export class ReactPyDjangoClient
+ extends BaseReactPyClient
+ implements ReactPyClient
+{
+ urls: ReactPyUrls;
+ socket: { current?: WebSocket };
+
+ constructor(props: ReactPyDjangoClientProps) {
+ super();
+ this.urls = props.urls;
+ this.socket = createReconnectingWebSocket({
+ readyPromise: this.ready,
+ url: this.urls.componentUrl,
+ onMessage: async ({ data }) =>
+ this.handleIncoming(JSON.parse(data)),
+ ...props.reconnectOptions,
+ });
+ }
+
+ sendMessage(message: any): void {
+ this.socket.current?.send(JSON.stringify(message));
+ }
+
+ loadModule(moduleName: string) {
+ return import(`${this.urls.jsModules}/${moduleName}`);
+ }
+}
+
+export function mountComponent(
+ mountElement: HTMLElement,
+ host: string,
+ urlPrefix: string,
+ componentPath: string,
+ resolvedJsModulesPath: string,
+ reconnectMaxInterval: number,
+ reconnectJitterMultiplier: number,
+ reconnectBackoffMultiplier: number,
+ reconnectMaxRetries: number
+) {
+ // Protocols
+ let httpProtocol = window.location.protocol;
+ let wsProtocol = `ws${httpProtocol === "https:" ? "s" : ""}:`;
+
+ // WebSocket route (for Python components)
+ let wsOrigin: string;
+ if (host) {
+ wsOrigin = `${wsProtocol}//${host}`;
+ } else {
+ wsOrigin = `${wsProtocol}//${window.location.host}`;
+ }
+
+ // HTTP route (for JavaScript modules)
+ let httpOrigin: string;
+ let jsModulesPath: string;
+ if (host) {
+ httpOrigin = `${httpProtocol}//${host}`;
+ jsModulesPath = `${urlPrefix}/web_module`;
+ } else {
+ httpOrigin = `${httpProtocol}//${window.location.host}`;
+ if (resolvedJsModulesPath) {
+ jsModulesPath = resolvedJsModulesPath;
+ } else {
+ jsModulesPath = `${urlPrefix}/web_module`;
+ }
+ }
+
+ // Set up the client
+ const client = new ReactPyDjangoClient({
+ urls: {
+ componentUrl: `${wsOrigin}/${urlPrefix}/${componentPath}`,
+ query: document.location.search,
+ jsModules: `${httpOrigin}/${jsModulesPath}`,
+ },
+ reconnectOptions: {
+ maxInterval: reconnectMaxInterval,
+ intervalJitter: reconnectJitterMultiplier,
+ backoffRate: reconnectBackoffMultiplier,
+ maxRetries: reconnectMaxRetries,
+ },
+ });
+
+ // Start rendering the component
+ mount(mountElement, client);
+}
diff --git a/src/js/src/types.ts b/src/js/src/types.ts
new file mode 100644
index 00000000..576b1fc7
--- /dev/null
+++ b/src/js/src/types.ts
@@ -0,0 +1,17 @@
+export type ReconnectOptions = {
+ maxInterval?: number;
+ maxRetries?: number;
+ backoffRate?: number;
+ intervalJitter?: number;
+}
+
+export type ReactPyUrls = {
+ componentUrl: string;
+ query: string;
+ jsModules: string;
+}
+
+export type ReactPyDjangoClientProps = {
+ urls: ReactPyUrls;
+ reconnectOptions: ReconnectOptions;
+}
diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts
new file mode 100644
index 00000000..1bbacc96
--- /dev/null
+++ b/src/js/src/utils.ts
@@ -0,0 +1,100 @@
+export function createReconnectingWebSocket(props: {
+ url: string;
+ readyPromise: Promise;
+ onOpen?: () => void;
+ onMessage: (message: MessageEvent) => void;
+ onClose?: () => void;
+ maxInterval?: number;
+ maxRetries?: number;
+ backoffRate?: number;
+ intervalJitter?: number;
+}) {
+ const {
+ maxInterval = 60000,
+ maxRetries = 50,
+ backoffRate = 1.1,
+ intervalJitter = 0.1,
+ } = props;
+
+ const startInterval = 750;
+ let retries = 0;
+ let interval = startInterval;
+ const closed = false;
+ let everConnected = false;
+ const socket: { current?: WebSocket } = {};
+
+ const connect = () => {
+ if (closed) {
+ return;
+ }
+ socket.current = new WebSocket(props.url);
+ socket.current.onopen = () => {
+ everConnected = true;
+ console.info("ReactPy connected!");
+ interval = startInterval;
+ retries = 0;
+ if (props.onOpen) {
+ props.onOpen();
+ }
+ };
+ socket.current.onmessage = props.onMessage;
+ socket.current.onclose = () => {
+ if (!everConnected) {
+ console.info("ReactPy failed to connect!");
+ return;
+ }
+
+ console.info("ReactPy disconnected!");
+ if (props.onClose) {
+ props.onClose();
+ }
+
+ if (retries >= maxRetries) {
+ return;
+ }
+
+ const thisInterval = addJitter(interval, intervalJitter);
+ console.info(
+ `ReactPy reconnecting in ${(thisInterval / 1000).toPrecision(
+ 4
+ )} seconds...`
+ );
+ setTimeout(connect, thisInterval);
+ interval = nextInterval(interval, backoffRate, maxInterval);
+ retries++;
+ };
+ };
+
+ props.readyPromise
+ .then(() => console.info("Starting ReactPy client..."))
+ .then(connect);
+
+ return socket;
+}
+
+export function nextInterval(
+ currentInterval: number,
+ backoffRate: number,
+ maxInterval: number
+): number {
+ return Math.min(
+ currentInterval *
+ // increase interval by backoff rate
+ backoffRate,
+ // don't exceed max interval
+ maxInterval
+ );
+}
+
+export function addJitter(interval: number, jitter: number): number {
+ return (
+ interval + (Math.random() * jitter * interval * 2 - jitter * interval)
+ );
+}
+
+type reconnectOptions = {
+ maxInterval?: number;
+ maxRetries?: number;
+ backoffRate?: number;
+ intervalJitter?: number;
+};
diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json
new file mode 100644
index 00000000..7da4aa77
--- /dev/null
+++ b/src/js/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "module": "esnext",
+ "moduleResolution": "node",
+ },
+}
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index 7ab9546e..efb35e35 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -204,12 +204,12 @@ def reactpy_errors(app_configs, **kwargs):
id="reactpy_django.E003",
)
)
- if not isinstance(getattr(settings, "REACTPY_RECONNECT_MAX", 0), int):
+ if not isinstance(getattr(settings, "REACTPY_SESSION_MAX_AGE", 0), int):
errors.append(
Error(
- "Invalid type for REACTPY_RECONNECT_MAX.",
- hint="REACTPY_RECONNECT_MAX should be an integer.",
- obj=settings.REACTPY_RECONNECT_MAX,
+ "Invalid type for REACTPY_SESSION_MAX_AGE.",
+ hint="REACTPY_SESSION_MAX_AGE should be an integer.",
+ obj=settings.REACTPY_SESSION_MAX_AGE,
id="reactpy_django.E004",
)
)
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index 24aff5f6..ae8c8306 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -28,6 +28,11 @@
"REACTPY_WEBSOCKET_URL",
"reactpy/",
)
+REACTPY_RECONNECT_MAX: int = getattr(
+ settings,
+ "REACTPY_RECONNECT_MAX",
+ 259200, # Default to 3 days
+)
# Configurable through Django settings.py
REACTPY_URL_PREFIX: str = getattr(
@@ -35,10 +40,10 @@
"REACTPY_URL_PREFIX",
REACTPY_WEBSOCKET_URL,
).strip("/")
-REACTPY_RECONNECT_MAX: int = getattr(
+REACTPY_SESSION_MAX_AGE: int = getattr(
settings,
- "REACTPY_RECONNECT_MAX",
- 259200, # Default to 3 days
+ "REACTPY_SESSION_MAX_AGE",
+ REACTPY_RECONNECT_MAX,
)
REACTPY_CACHE: str = getattr(
settings,
@@ -82,3 +87,23 @@
if _default_hosts
else None
)
+REACTPY_RECONNECT_MAX_INTERVAL: int = getattr(
+ settings,
+ "REACTPY_RECONNECT_MAX_INTERVAL",
+ 60000, # Default to 60 seconds
+)
+REACTPY_RECONNECT_JITTER_MULTIPLIER: int = getattr(
+ settings,
+ "REACTPY_RECONNECT_JITTER_MULTIPLIER",
+ 0, # Default to no jitter
+)
+REACTPY_RECONNECT_BACKOFF_MULTIPLIER: int = getattr(
+ settings,
+ "REACTPY_RECONNECT_BACKOFF_MULTIPLIER",
+ 1.1, # Default to 10% backoff
+)
+REACTPY_RECONNECT_MAX_RETRIES: int = getattr(
+ settings,
+ "REACTPY_RECONNECT_MAX_RETRIES",
+ 150,
+)
diff --git a/src/reactpy_django/migrations/0005_alter_componentsession_last_accessed.py b/src/reactpy_django/migrations/0005_alter_componentsession_last_accessed.py
new file mode 100644
index 00000000..488f660d
--- /dev/null
+++ b/src/reactpy_django/migrations/0005_alter_componentsession_last_accessed.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.3 on 2023-08-23 19:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("reactpy_django", "0004_config"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="componentsession",
+ name="last_accessed",
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
diff --git a/src/reactpy_django/models.py b/src/reactpy_django/models.py
index 65152126..1fa69d2c 100644
--- a/src/reactpy_django/models.py
+++ b/src/reactpy_django/models.py
@@ -8,7 +8,7 @@ class ComponentSession(models.Model):
uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore
params = models.BinaryField(editable=False) # type: ignore
- last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore
+ last_accessed = models.DateTimeField(auto_now=True) # type: ignore
class Config(models.Model):
diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html
index 4010b80f..4d98bfd4 100644
--- a/src/reactpy_django/templates/reactpy/component.html
+++ b/src/reactpy_django/templates/reactpy/component.html
@@ -6,15 +6,18 @@
{% else %}
{% endif %}
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index 1ae88413..4852a52b 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -84,7 +84,7 @@ def component(
_logger.error(msg)
return failure_context(dotted_path, ComponentDoesNotExistError(msg))
- # Validate the component
+ # Validate the component args & kwargs
if is_local and config.REACTPY_DEBUG_MODE:
try:
validate_component_args(user_component, *args, **kwargs)
@@ -109,11 +109,14 @@ def component(
"reactpy_uuid": uuid,
"reactpy_host": host or perceived_host,
"reactpy_url_prefix": config.REACTPY_URL_PREFIX,
- "reactpy_reconnect_max": config.REACTPY_RECONNECT_MAX,
"reactpy_component_path": f"{dotted_path}/{uuid}/"
if component_has_args
else f"{dotted_path}/",
"reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
+ "reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL,
+ "reactpy_reconnect_jitter_multiplier": config.REACTPY_RECONNECT_JITTER_MULTIPLIER,
+ "reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
+ "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_INTERVAL,
}
diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py
index 0776bc3b..f40ea1ee 100644
--- a/src/reactpy_django/utils.py
+++ b/src/reactpy_django/utils.py
@@ -327,17 +327,17 @@ def create_cache_key(*args):
def db_cleanup(immediate: bool = False):
"""Deletes expired component sessions from the database.
This function may be expanded in the future to include additional cleanup tasks."""
- from .config import REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX
+ from .config import REACTPY_DEBUG_MODE, REACTPY_SESSION_MAX_AGE
from .models import ComponentSession, Config
config = Config.load()
start_time = timezone.now()
cleaned_at = config.cleaned_at
- clean_needed_by = cleaned_at + timedelta(seconds=REACTPY_RECONNECT_MAX)
+ clean_needed_by = cleaned_at + timedelta(seconds=REACTPY_SESSION_MAX_AGE)
# Delete expired component parameters
if immediate or timezone.now() >= clean_needed_by:
- expiration_date = timezone.now() - timedelta(seconds=REACTPY_RECONNECT_MAX)
+ expiration_date = timezone.now() - timedelta(seconds=REACTPY_SESSION_MAX_AGE)
ComponentSession.objects.filter(last_accessed__lte=expiration_date).delete()
config.cleaned_at = timezone.now()
config.save()
diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py
index a2a76cab..b486dc3c 100644
--- a/src/reactpy_django/websocket/consumer.py
+++ b/src/reactpy_django/websocket/consumer.py
@@ -42,6 +42,7 @@ class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
async def connect(self) -> None:
"""The browser has connected."""
+ from reactpy_django import models
from reactpy_django.config import REACTPY_AUTH_BACKEND, REACTPY_BACKHAUL_THREAD
await super().connect()
@@ -80,6 +81,7 @@ async def connect(self) -> None:
# Start the component dispatcher
self.dispatcher: Future | asyncio.Task
self.threaded = REACTPY_BACKHAUL_THREAD
+ self.component_session: models.ComponentSession | None = None
if self.threaded:
if not backhaul_thread.is_alive():
await asyncio.to_thread(
@@ -95,6 +97,17 @@ async def connect(self) -> None:
async def disconnect(self, code: int) -> None:
"""The browser has disconnected."""
self.dispatcher.cancel()
+
+ # Update the last_accessed timestamp
+ if self.component_session:
+ try:
+ await self.component_session.asave()
+ except Exception:
+ await asyncio.to_thread(
+ _logger.exception,
+ "ReactPy has failed to update component session `last_accessed`!",
+ )
+
await super().disconnect(code)
async def receive_json(self, content: Any, **_) -> None:
@@ -118,8 +131,8 @@ async def run_dispatcher(self):
"""Runs the main loop that performs component rendering tasks."""
from reactpy_django import models
from reactpy_django.config import (
- REACTPY_RECONNECT_MAX,
REACTPY_REGISTERED_COMPONENTS,
+ REACTPY_SESSION_MAX_AGE,
)
scope = self.scope
@@ -155,16 +168,14 @@ async def run_dispatcher(self):
# Always clean up expired entries first
await database_sync_to_async(db_cleanup, thread_sensitive=False)()
- # Get the queries from a DB
- params_query = await models.ComponentSession.objects.aget(
+ # Get the component session from the DB
+ self.component_session = await models.ComponentSession.objects.aget(
uuid=uuid,
- last_accessed__gt=now - timedelta(seconds=REACTPY_RECONNECT_MAX),
+ last_accessed__gt=now - timedelta(seconds=REACTPY_SESSION_MAX_AGE),
+ )
+ component_params: ComponentParamData = pickle.loads(
+ self.component_session.params
)
- params_query.last_accessed = timezone.now()
- await database_sync_to_async(
- params_query.save, thread_sensitive=False
- )()
- component_params: ComponentParamData = pickle.loads(params_query.params)
component_args = component_params.args
component_kwargs = component_params.kwargs
@@ -176,7 +187,7 @@ async def run_dispatcher(self):
await asyncio.to_thread(
_logger.warning,
f"Component session for '{dotted_path}:{uuid}' not found. The "
- "session may have already expired beyond REACTPY_RECONNECT_MAX. "
+ "session may have already expired beyond REACTPY_SESSION_MAX_AGE. "
"If you are using a custom host, you may have forgotten to provide "
"args/kwargs.",
)
@@ -185,7 +196,7 @@ async def run_dispatcher(self):
await asyncio.to_thread(
_logger.exception,
f"Failed to construct component {component_constructor} "
- f"with parameters {component_kwargs}",
+ f"with args='{component_args}' kwargs='{component_kwargs}'!",
)
return
diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py
index 3bd23527..94f75950 100644
--- a/tests/test_app/tests/test_database.py
+++ b/tests/test_app/tests/test_database.py
@@ -31,8 +31,8 @@ def test_component_params(self):
# Force `params_1` to expire
from reactpy_django import config
- config.REACTPY_RECONNECT_MAX = 1
- sleep(config.REACTPY_RECONNECT_MAX + 0.1)
+ config.REACTPY_SESSION_MAX_AGE = 1
+ sleep(config.REACTPY_SESSION_MAX_AGE + 0.1)
# Create a new, non-expired component params
params_2 = self._save_params_to_db(2)
From 4d93310b30fd5488765acdc2cc3d67c7fae7e5b2 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 17:50:20 -0700
Subject: [PATCH 02/16] refactoring
---
src/js/src/client.ts | 31 ++++++++++++++
src/js/src/index.ts | 41 ++++---------------
src/js/src/types.ts | 5 ++-
src/js/src/utils.ts | 35 +++++++---------
src/reactpy_django/config.py | 5 +++
.../templates/reactpy/component.html | 1 +
src/reactpy_django/templatetags/reactpy.py | 1 +
7 files changed, 62 insertions(+), 57 deletions(-)
create mode 100644 src/js/src/client.ts
diff --git a/src/js/src/client.ts b/src/js/src/client.ts
new file mode 100644
index 00000000..eb8b3929
--- /dev/null
+++ b/src/js/src/client.ts
@@ -0,0 +1,31 @@
+import { BaseReactPyClient, ReactPyClient } from "@reactpy/client";
+import { createReconnectingWebSocket } from "./utils";
+import { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
+
+export class ReactPyDjangoClient
+ extends BaseReactPyClient
+ implements ReactPyClient
+{
+ urls: ReactPyUrls;
+ socket: { current?: WebSocket };
+
+ constructor(props: ReactPyDjangoClientProps) {
+ super();
+ this.urls = props.urls;
+ this.socket = createReconnectingWebSocket({
+ readyPromise: this.ready,
+ url: this.urls.componentUrl,
+ onMessage: async ({ data }) =>
+ this.handleIncoming(JSON.parse(data)),
+ ...props.reconnectOptions,
+ });
+ }
+
+ sendMessage(message: any): void {
+ this.socket.current?.send(JSON.stringify(message));
+ }
+
+ loadModule(moduleName: string) {
+ return import(`${this.urls.jsModules}/${moduleName}`);
+ }
+}
diff --git a/src/js/src/index.ts b/src/js/src/index.ts
index d19dbf79..8c45da78 100644
--- a/src/js/src/index.ts
+++ b/src/js/src/index.ts
@@ -1,34 +1,5 @@
-import { mount, BaseReactPyClient, ReactPyClient } from "@reactpy/client";
-import { createReconnectingWebSocket } from "./utils";
-import { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
-
-export class ReactPyDjangoClient
- extends BaseReactPyClient
- implements ReactPyClient
-{
- urls: ReactPyUrls;
- socket: { current?: WebSocket };
-
- constructor(props: ReactPyDjangoClientProps) {
- super();
- this.urls = props.urls;
- this.socket = createReconnectingWebSocket({
- readyPromise: this.ready,
- url: this.urls.componentUrl,
- onMessage: async ({ data }) =>
- this.handleIncoming(JSON.parse(data)),
- ...props.reconnectOptions,
- });
- }
-
- sendMessage(message: any): void {
- this.socket.current?.send(JSON.stringify(message));
- }
-
- loadModule(moduleName: string) {
- return import(`${this.urls.jsModules}/${moduleName}`);
- }
-}
+import { mount } from "@reactpy/client";
+import { ReactPyDjangoClient } from "./client";
export function mountComponent(
mountElement: HTMLElement,
@@ -36,6 +7,7 @@ export function mountComponent(
urlPrefix: string,
componentPath: string,
resolvedJsModulesPath: string,
+ reconnectStartInterval: number,
reconnectMaxInterval: number,
reconnectJitterMultiplier: number,
reconnectBackoffMultiplier: number,
@@ -68,7 +40,7 @@ export function mountComponent(
}
}
- // Set up the client
+ // Configure a new ReactPy client
const client = new ReactPyDjangoClient({
urls: {
componentUrl: `${wsOrigin}/${urlPrefix}/${componentPath}`,
@@ -76,9 +48,10 @@ export function mountComponent(
jsModules: `${httpOrigin}/${jsModulesPath}`,
},
reconnectOptions: {
+ startInterval: reconnectStartInterval,
maxInterval: reconnectMaxInterval,
- intervalJitter: reconnectJitterMultiplier,
- backoffRate: reconnectBackoffMultiplier,
+ jitterMultiplier: reconnectJitterMultiplier,
+ backoffMultiplier: reconnectBackoffMultiplier,
maxRetries: reconnectMaxRetries,
},
});
diff --git a/src/js/src/types.ts b/src/js/src/types.ts
index 576b1fc7..7394d7e3 100644
--- a/src/js/src/types.ts
+++ b/src/js/src/types.ts
@@ -1,8 +1,9 @@
export type ReconnectOptions = {
+ startInterval?: number;
maxInterval?: number;
maxRetries?: number;
- backoffRate?: number;
- intervalJitter?: number;
+ backoffMultiplier?: number;
+ jitterMultiplier?: number;
}
export type ReactPyUrls = {
diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts
index 1bbacc96..063fc697 100644
--- a/src/js/src/utils.ts
+++ b/src/js/src/utils.ts
@@ -4,23 +4,23 @@ export function createReconnectingWebSocket(props: {
onOpen?: () => void;
onMessage: (message: MessageEvent) => void;
onClose?: () => void;
+ startInterval?: number;
maxInterval?: number;
maxRetries?: number;
- backoffRate?: number;
- intervalJitter?: number;
+ backoffMultiplier?: number;
+ jitterMultiplier?: number;
}) {
const {
- maxInterval = 60000,
- maxRetries = 50,
- backoffRate = 1.1,
- intervalJitter = 0.1,
+ startInterval,
+ maxInterval,
+ maxRetries,
+ backoffMultiplier,
+ jitterMultiplier,
} = props;
-
- const startInterval = 750;
let retries = 0;
let interval = startInterval;
- const closed = false;
let everConnected = false;
+ const closed = false;
const socket: { current?: WebSocket } = {};
const connect = () => {
@@ -53,14 +53,14 @@ export function createReconnectingWebSocket(props: {
return;
}
- const thisInterval = addJitter(interval, intervalJitter);
+ const thisInterval = addJitter(interval, jitterMultiplier);
console.info(
`ReactPy reconnecting in ${(thisInterval / 1000).toPrecision(
4
)} seconds...`
);
setTimeout(connect, thisInterval);
- interval = nextInterval(interval, backoffRate, maxInterval);
+ interval = nextInterval(interval, backoffMultiplier, maxInterval);
retries++;
};
};
@@ -74,13 +74,13 @@ export function createReconnectingWebSocket(props: {
export function nextInterval(
currentInterval: number,
- backoffRate: number,
+ backoffMultiplier: number,
maxInterval: number
): number {
return Math.min(
currentInterval *
- // increase interval by backoff rate
- backoffRate,
+ // increase interval by backoff multiplier
+ backoffMultiplier,
// don't exceed max interval
maxInterval
);
@@ -91,10 +91,3 @@ export function addJitter(interval: number, jitter: number): number {
interval + (Math.random() * jitter * interval * 2 - jitter * interval)
);
}
-
-type reconnectOptions = {
- maxInterval?: number;
- maxRetries?: number;
- backoffRate?: number;
- intervalJitter?: number;
-};
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index ae8c8306..0045889f 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -87,6 +87,11 @@
if _default_hosts
else None
)
+REACTPY_RECONNECT_INTERVAL: int = getattr(
+ settings,
+ "REACTPY_RECONNECT_INTERVAL",
+ 750, # Default to 0.75 seconds
+)
REACTPY_RECONNECT_MAX_INTERVAL: int = getattr(
settings,
"REACTPY_RECONNECT_MAX_INTERVAL",
diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html
index 4d98bfd4..be4950a2 100644
--- a/src/reactpy_django/templates/reactpy/component.html
+++ b/src/reactpy_django/templates/reactpy/component.html
@@ -14,6 +14,7 @@
"{{ reactpy_url_prefix }}",
"{{ reactpy_component_path }}",
"{{ reactpy_resolved_web_modules_path }}",
+ Number("{{ reactpy_reconnect_interval }}"),
Number("{{ reactpy_reconnect_max_interval }}"),
Number("{{ reactpy_reconnect_jitter_multiplier }}"),
Number("{{ reactpy_reconnect_backoff_multiplier }}"),
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index 4852a52b..7f6e4e50 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -113,6 +113,7 @@ def component(
if component_has_args
else f"{dotted_path}/",
"reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
+ "reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL,
"reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL,
"reactpy_reconnect_jitter_multiplier": config.REACTPY_RECONNECT_JITTER_MULTIPLIER,
"reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
From 93f4f8058260a46ce8165984c301be107b4c411d Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 21:49:51 -0700
Subject: [PATCH 03/16] fix types
---
pyproject.toml | 1 -
src/js/src/types.ts | 10 +++++-----
src/js/src/utils.ts | 10 +++++-----
src/reactpy_django/config.py | 4 ++--
4 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 0f9e87a3..250a64a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,6 @@ warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
check_untyped_defs = true
-incremental = true
[tool.ruff.isort]
known-first-party = ["src", "tests"]
diff --git a/src/js/src/types.ts b/src/js/src/types.ts
index 7394d7e3..2cb9fa2b 100644
--- a/src/js/src/types.ts
+++ b/src/js/src/types.ts
@@ -1,9 +1,9 @@
export type ReconnectOptions = {
- startInterval?: number;
- maxInterval?: number;
- maxRetries?: number;
- backoffMultiplier?: number;
- jitterMultiplier?: number;
+ startInterval: number;
+ maxInterval: number;
+ maxRetries: number;
+ backoffMultiplier: number;
+ jitterMultiplier: number;
}
export type ReactPyUrls = {
diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts
index 063fc697..fb430d01 100644
--- a/src/js/src/utils.ts
+++ b/src/js/src/utils.ts
@@ -4,11 +4,11 @@ export function createReconnectingWebSocket(props: {
onOpen?: () => void;
onMessage: (message: MessageEvent) => void;
onClose?: () => void;
- startInterval?: number;
- maxInterval?: number;
- maxRetries?: number;
- backoffMultiplier?: number;
- jitterMultiplier?: number;
+ startInterval: number;
+ maxInterval: number;
+ maxRetries: number;
+ backoffMultiplier: number;
+ jitterMultiplier: number;
}) {
const {
startInterval,
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index 0045889f..3c429e9c 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -97,12 +97,12 @@
"REACTPY_RECONNECT_MAX_INTERVAL",
60000, # Default to 60 seconds
)
-REACTPY_RECONNECT_JITTER_MULTIPLIER: int = getattr(
+REACTPY_RECONNECT_JITTER_MULTIPLIER: float | int = getattr(
settings,
"REACTPY_RECONNECT_JITTER_MULTIPLIER",
0, # Default to no jitter
)
-REACTPY_RECONNECT_BACKOFF_MULTIPLIER: int = getattr(
+REACTPY_RECONNECT_BACKOFF_MULTIPLIER: float | int = getattr(
settings,
"REACTPY_RECONNECT_BACKOFF_MULTIPLIER",
1.1, # Default to 10% backoff
From c89f437a80a263759c0e7c8a61550e3df7fdcd44 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 22:29:28 -0700
Subject: [PATCH 04/16] fix max retries
---
src/js/src/client.ts | 4 ++--
src/js/src/index.ts | 4 ++--
src/js/src/utils.ts | 1 +
src/reactpy_django/templates/reactpy/component.html | 4 ++--
src/reactpy_django/templatetags/reactpy.py | 2 +-
5 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/src/js/src/client.ts b/src/js/src/client.ts
index eb8b3929..6f79df77 100644
--- a/src/js/src/client.ts
+++ b/src/js/src/client.ts
@@ -1,4 +1,4 @@
-import { BaseReactPyClient, ReactPyClient } from "@reactpy/client";
+import { BaseReactPyClient, ReactPyClient, ReactPyModule } from "@reactpy/client";
import { createReconnectingWebSocket } from "./utils";
import { ReactPyDjangoClientProps, ReactPyUrls } from "./types";
@@ -25,7 +25,7 @@ export class ReactPyDjangoClient
this.socket.current?.send(JSON.stringify(message));
}
- loadModule(moduleName: string) {
+ loadModule(moduleName: string): Promise {
return import(`${this.urls.jsModules}/${moduleName}`);
}
}
diff --git a/src/js/src/index.ts b/src/js/src/index.ts
index 8c45da78..65ac4611 100644
--- a/src/js/src/index.ts
+++ b/src/js/src/index.ts
@@ -9,9 +9,9 @@ export function mountComponent(
resolvedJsModulesPath: string,
reconnectStartInterval: number,
reconnectMaxInterval: number,
- reconnectJitterMultiplier: number,
+ reconnectMaxRetries: number,
reconnectBackoffMultiplier: number,
- reconnectMaxRetries: number
+ reconnectJitterMultiplier: number
) {
// Protocols
let httpProtocol = window.location.protocol;
diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts
index fb430d01..1d2b7ca1 100644
--- a/src/js/src/utils.ts
+++ b/src/js/src/utils.ts
@@ -50,6 +50,7 @@ export function createReconnectingWebSocket(props: {
}
if (retries >= maxRetries) {
+ console.info("ReactPy connection max retries exhausted!");
return;
}
diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html
index be4950a2..f7f9e6a6 100644
--- a/src/reactpy_django/templates/reactpy/component.html
+++ b/src/reactpy_django/templates/reactpy/component.html
@@ -16,9 +16,9 @@
"{{ reactpy_resolved_web_modules_path }}",
Number("{{ reactpy_reconnect_interval }}"),
Number("{{ reactpy_reconnect_max_interval }}"),
- Number("{{ reactpy_reconnect_jitter_multiplier }}"),
- Number("{{ reactpy_reconnect_backoff_multiplier }}"),
Number("{{ reactpy_reconnect_max_retries }}"),
+ Number("{{ reactpy_reconnect_backoff_multiplier }}"),
+ Number("{{ reactpy_reconnect_jitter_multiplier }}"),
);
{% endif %}
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index 7f6e4e50..97d53a17 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -117,7 +117,7 @@ def component(
"reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL,
"reactpy_reconnect_jitter_multiplier": config.REACTPY_RECONNECT_JITTER_MULTIPLIER,
"reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
- "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_INTERVAL,
+ "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_RETRIES,
}
From 92a9acfe1ddcbe55de83ce277bcef02e5afd128d Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 22:58:33 -0700
Subject: [PATCH 05/16] remove jtter
---
src/js/src/index.ts | 4 +---
src/js/src/types.ts | 1 -
src/js/src/utils.ts | 23 +++----------------
src/reactpy_django/config.py | 13 ++++-------
.../templates/reactpy/component.html | 1 -
src/reactpy_django/templatetags/reactpy.py | 1 -
6 files changed, 8 insertions(+), 35 deletions(-)
diff --git a/src/js/src/index.ts b/src/js/src/index.ts
index 65ac4611..53d67c6f 100644
--- a/src/js/src/index.ts
+++ b/src/js/src/index.ts
@@ -10,8 +10,7 @@ export function mountComponent(
reconnectStartInterval: number,
reconnectMaxInterval: number,
reconnectMaxRetries: number,
- reconnectBackoffMultiplier: number,
- reconnectJitterMultiplier: number
+ reconnectBackoffMultiplier: number
) {
// Protocols
let httpProtocol = window.location.protocol;
@@ -50,7 +49,6 @@ export function mountComponent(
reconnectOptions: {
startInterval: reconnectStartInterval,
maxInterval: reconnectMaxInterval,
- jitterMultiplier: reconnectJitterMultiplier,
backoffMultiplier: reconnectBackoffMultiplier,
maxRetries: reconnectMaxRetries,
},
diff --git a/src/js/src/types.ts b/src/js/src/types.ts
index 2cb9fa2b..54a0b604 100644
--- a/src/js/src/types.ts
+++ b/src/js/src/types.ts
@@ -3,7 +3,6 @@ export type ReconnectOptions = {
maxInterval: number;
maxRetries: number;
backoffMultiplier: number;
- jitterMultiplier: number;
}
export type ReactPyUrls = {
diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts
index 1d2b7ca1..a3f653ce 100644
--- a/src/js/src/utils.ts
+++ b/src/js/src/utils.ts
@@ -8,15 +8,8 @@ export function createReconnectingWebSocket(props: {
maxInterval: number;
maxRetries: number;
backoffMultiplier: number;
- jitterMultiplier: number;
}) {
- const {
- startInterval,
- maxInterval,
- maxRetries,
- backoffMultiplier,
- jitterMultiplier,
- } = props;
+ const { startInterval, maxInterval, maxRetries, backoffMultiplier } = props;
let retries = 0;
let interval = startInterval;
let everConnected = false;
@@ -43,24 +36,20 @@ export function createReconnectingWebSocket(props: {
console.info("ReactPy failed to connect!");
return;
}
-
console.info("ReactPy disconnected!");
if (props.onClose) {
props.onClose();
}
-
if (retries >= maxRetries) {
console.info("ReactPy connection max retries exhausted!");
return;
}
-
- const thisInterval = addJitter(interval, jitterMultiplier);
console.info(
- `ReactPy reconnecting in ${(thisInterval / 1000).toPrecision(
+ `ReactPy reconnecting in ${(interval / 1000).toPrecision(
4
)} seconds...`
);
- setTimeout(connect, thisInterval);
+ setTimeout(connect, interval);
interval = nextInterval(interval, backoffMultiplier, maxInterval);
retries++;
};
@@ -86,9 +75,3 @@ export function nextInterval(
maxInterval
);
}
-
-export function addJitter(interval: number, jitter: number): number {
- return (
- interval + (Math.random() * jitter * interval * 2 - jitter * interval)
- );
-}
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index 3c429e9c..695e78f4 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -97,18 +97,13 @@
"REACTPY_RECONNECT_MAX_INTERVAL",
60000, # Default to 60 seconds
)
-REACTPY_RECONNECT_JITTER_MULTIPLIER: float | int = getattr(
+REACTPY_RECONNECT_MAX_RETRIES: int = getattr(
settings,
- "REACTPY_RECONNECT_JITTER_MULTIPLIER",
- 0, # Default to no jitter
+ "REACTPY_RECONNECT_MAX_RETRIES",
+ 150,
)
REACTPY_RECONNECT_BACKOFF_MULTIPLIER: float | int = getattr(
settings,
"REACTPY_RECONNECT_BACKOFF_MULTIPLIER",
- 1.1, # Default to 10% backoff
-)
-REACTPY_RECONNECT_MAX_RETRIES: int = getattr(
- settings,
- "REACTPY_RECONNECT_MAX_RETRIES",
- 150,
+ 1.25, # Default to 10% backoff
)
diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html
index f7f9e6a6..75a65dbe 100644
--- a/src/reactpy_django/templates/reactpy/component.html
+++ b/src/reactpy_django/templates/reactpy/component.html
@@ -18,7 +18,6 @@
Number("{{ reactpy_reconnect_max_interval }}"),
Number("{{ reactpy_reconnect_max_retries }}"),
Number("{{ reactpy_reconnect_backoff_multiplier }}"),
- Number("{{ reactpy_reconnect_jitter_multiplier }}"),
);
{% endif %}
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index 97d53a17..74d2c2db 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -115,7 +115,6 @@ def component(
"reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
"reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL,
"reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL,
- "reactpy_reconnect_jitter_multiplier": config.REACTPY_RECONNECT_JITTER_MULTIPLIER,
"reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
"reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_RETRIES,
}
From 28570771d61e8df8bc0c72309372109a9ab0785d Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 22:58:45 -0700
Subject: [PATCH 06/16] add checks for REACTPY_RECONNECT_MAX
---
src/reactpy_django/checks.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index efb35e35..3e5c42b2 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -104,7 +104,7 @@ def reactpy_warnings(app_configs, **kwargs):
# DELETED W007: Check if REACTPY_WEBSOCKET_URL doesn't end with a slash
# DELETED W008: Check if REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character
- # Removed Settings
+ # Removed REACTPY_WEBSOCKET_URL setting
if getattr(settings, "REACTPY_WEBSOCKET_URL", None):
warnings.append(
Warning(
@@ -159,6 +159,16 @@ def reactpy_warnings(app_configs, **kwargs):
)
)
+ # Removed REACTPY_RECONNECT_MAX setting
+ if getattr(settings, "REACTPY_RECONNECT_MAX", None):
+ warnings.append(
+ Warning(
+ "REACTPY_RECONNECT_MAX has been removed.",
+ hint="Use REACTPY_SESSION_MAX_AGE as an alternative.",
+ id="reactpy_django.W013",
+ )
+ )
+
return warnings
From a7c92c3fb1c22c07f1b2180fd4fc6b68cf63ce36 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 23 Aug 2023 23:06:47 -0700
Subject: [PATCH 07/16] add docs
---
docs/src/features/settings.md | 6 +++++-
src/reactpy_django/config.py | 2 +-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md
index a68ad25c..288f3a68 100644
--- a/docs/src/features/settings.md
+++ b/docs/src/features/settings.md
@@ -14,12 +14,16 @@ These are ReactPy-Django's default settings values. You can modify these values
| --- | --- | --- | --- |
| `REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache.
We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
| `REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database ReactPy uses to store session data. ReactPy requires a multiprocessing-safe and thread-safe database.
If configuring `REACTPY_DATABASE`, it is mandatory to also configure `DATABASE_ROUTERS` like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
-| `REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 96000`, `#!python 60`, `#!python 0` | Maximum seconds between reconnection attempts before giving up.
Use `#!python 0` to prevent reconnection. |
+| `REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `args` and `kwargs` passed into your component template tag.
Use `#!python 0` to not store any session data. |
| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. |
| `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. |
| `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `AuthMiddlewareStack` and...
2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `backend` attribute. |
| `REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the webserver to process web traffic while during ReactPy rendering.
Vastly improves throughput with web servers such as `hypercorn` and `uvicorn`. |
| `REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | Default host(s) to use for ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing.
You can use the `host` argument in your [template tag](../features/template-tag.md#component) to override this default. |
+| `REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. |
+| `REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. |
+| `REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. |
+| `REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. |
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index 695e78f4..d8ca67b4 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -105,5 +105,5 @@
REACTPY_RECONNECT_BACKOFF_MULTIPLIER: float | int = getattr(
settings,
"REACTPY_RECONNECT_BACKOFF_MULTIPLIER",
- 1.25, # Default to 10% backoff
+ 1.25, # Default to 25% backoff per connection attempt
)
From d2c7b56dd6fa14bd178184865a224746cd6235c5 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 24 Aug 2023 00:18:37 -0700
Subject: [PATCH 08/16] checks for all new settings
---
src/reactpy_django/checks.py | 171 +++++++++++++++++++++++++++++++++++
1 file changed, 171 insertions(+)
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index 3e5c42b2..2fc90f77 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -1,4 +1,5 @@
import contextlib
+import math
import sys
from django.contrib.staticfiles.finders import find
@@ -169,6 +170,77 @@ def reactpy_warnings(app_configs, **kwargs):
)
)
+ if (
+ isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
+ and config.REACTPY_RECONNECT_INTERVAL > 30000
+ ):
+ warnings.append(
+ Warning(
+ "REACTPY_RECONNECT_INTERVAL is set to >30 seconds. Are you sure this is intentional? "
+ "This may cause unexpected delays between reconnection.",
+ hint="Check your value for REACTPY_RECONNECT_INTERVAL or suppress this warning.",
+ id="reactpy_django.W014",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
+ and config.REACTPY_RECONNECT_MAX_RETRIES > 5000
+ ):
+ warnings.append(
+ Warning(
+ "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value. Are you sure this is intentional? "
+ "This may leave your clients attempting reconnections for a long time.",
+ hint="Check your value for REACTPY_RECONNECT_MAX_RETRIES or suppress this warning.",
+ id="reactpy_django.W015",
+ )
+ )
+
+ # Check if the value is too large (greater than 50)
+ if (
+ isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 100
+ ):
+ warnings.append(
+ Warning(
+ "REACTPY_RECONNECT_BACKOFF_MULTIPLIER is set to a very large value. Are you sure this is intentional?",
+ hint="Check your value for REACTPY_RECONNECT_BACKOFF_MULTIPLIER or suppress this warning.",
+ id="reactpy_django.W016",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
+ and isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
+ and isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
+ and isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
+ and config.REACTPY_RECONNECT_INTERVAL > 0
+ and config.REACTPY_RECONNECT_MAX_INTERVAL > 0
+ and config.REACTPY_RECONNECT_MAX_RETRIES > 0
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 1
+ and (
+ config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER
+ ** config.REACTPY_RECONNECT_MAX_RETRIES
+ )
+ * config.REACTPY_RECONNECT_INTERVAL
+ < config.REACTPY_RECONNECT_MAX_INTERVAL
+ ):
+ max_value = math.floor(
+ (
+ config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER
+ ** config.REACTPY_RECONNECT_MAX_RETRIES
+ )
+ * config.REACTPY_RECONNECT_INTERVAL
+ )
+ warnings.append(
+ Warning(
+ "Your current ReactPy configuration can never reach REACTPY_RECONNECT_MAX_INTERVAL. At most you will reach "
+ f"{max_value} miliseconds, which is less than {config.REACTPY_RECONNECT_MAX_INTERVAL} (REACTPY_RECONNECT_MAX_INTERVAL).",
+ hint="Check your ReactPy REACTPY_RECONNECT_* settings.",
+ id="reactpy_django.W017",
+ )
+ )
+
return warnings
@@ -176,6 +248,8 @@ def reactpy_warnings(app_configs, **kwargs):
def reactpy_errors(app_configs, **kwargs):
from django.conf import settings
+ from reactpy_django import config
+
errors = []
# Make sure ASGI is enabled
@@ -288,4 +362,101 @@ def reactpy_errors(app_configs, **kwargs):
)
break
+ if not isinstance(config.REACTPY_RECONNECT_INTERVAL, int):
+ errors.append(
+ Error(
+ "Invalid type for REACTPY_RECONNECT_INTERVAL.",
+ hint="REACTPY_RECONNECT_INTERVAL should be an integer.",
+ id="reactpy_django.E012",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
+ and config.REACTPY_RECONNECT_INTERVAL < 0
+ ):
+ errors.append(
+ Error(
+ "Invalid value for REACTPY_RECONNECT_INTERVAL.",
+ hint="REACTPY_RECONNECT_INTERVAL should be a positive integer.",
+ id="reactpy_django.E013",
+ )
+ )
+
+ if not isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int):
+ errors.append(
+ Error(
+ "Invalid type for REACTPY_RECONNECT_MAX_INTERVAL.",
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be an integer.",
+ id="reactpy_django.E014",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
+ and config.REACTPY_RECONNECT_MAX_INTERVAL < 0
+ ):
+ errors.append(
+ Error(
+ "Invalid value for REACTPY_RECONNECT_MAX_INTERVAL.",
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be a positive integer.",
+ id="reactpy_django.E015",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
+ and isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
+ and config.REACTPY_RECONNECT_MAX_INTERVAL < config.REACTPY_RECONNECT_INTERVAL
+ ):
+ errors.append(
+ Error(
+ "REACTPY_RECONNECT_MAX_INTERVAL is less than REACTPY_RECONNECT_INTERVAL.",
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be greater than or equal to REACTPY_RECONNECT_INTERVAL.",
+ id="reactpy_django.E016",
+ )
+ )
+
+ if not isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int):
+ errors.append(
+ Error(
+ "Invalid type for REACTPY_RECONNECT_MAX_RETRIES.",
+ hint="REACTPY_RECONNECT_MAX_RETRIES should be an integer.",
+ id="reactpy_django.E017",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
+ and config.REACTPY_RECONNECT_MAX_RETRIES < 0
+ ):
+ errors.append(
+ Error(
+ "Invalid value for REACTPY_RECONNECT_MAX_RETRIES.",
+ hint="REACTPY_RECONNECT_MAX_RETRIES should be a positive integer.",
+ id="reactpy_django.E018",
+ )
+ )
+
+ if not isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)):
+ errors.append(
+ Error(
+ "Invalid type for REACTPY_RECONNECT_BACKOFF_MULTIPLIER.",
+ hint="REACTPY_RECONNECT_BACKOFF_MULTIPLIER should be an integer or float.",
+ id="reactpy_django.E019",
+ )
+ )
+
+ if (
+ isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER < 1
+ ):
+ errors.append(
+ Error(
+ "Invalid value for REACTPY_RECONNECT_BACKOFF_MULTIPLIER.",
+ hint="REACTPY_RECONNECT_BACKOFF_MULTIPLIER should be greater than or equal to 1.",
+ id="reactpy_django.E020",
+ )
+ )
+
return errors
From 40511f404b76fad4e8cb76155f59c1bb484e3cb9 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 24 Aug 2023 00:46:02 -0700
Subject: [PATCH 09/16] changelog and tweaks
---
CHANGELOG.md | 16 ++++++++++++++++
src/reactpy_django/checks.py | 2 +-
src/reactpy_django/config.py | 7 +------
src/reactpy_django/websocket/consumer.py | 2 +-
4 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe62c373..7f179f70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,11 +34,27 @@ Using the following categories, list your changes in this order:
## [Unreleased]
+## Added
+
+- More reconnection behavior customizability through new settings!
+ - `REACTPY_RECONNECT_INTERVAL`
+ - `REACTPY_RECONNECT_MAX_INTERVAL`
+ - `REACTPY_RECONNECT_MAX_RETRIES`
+ - `REACTPY_RECONNECT_BACKOFF_MULTIPLIER`
+
### Changed
- Bumped the minimum ReactPy version to `1.0.2`.
- Prettier websocket URLs for components that do not have sessions.
- Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled.
+- Bumped the minimum `@reactpy/client` version to `0.3.1`
+- Use TypeScript instead of JavaScript for this repo.
+- Bumped minimum Django version to `4.2`.
+ - Note: Moving forward, ReactPy-Django will only support Django's latest versions to increase async support (and performance). This latest-only trend will continue until Django has all async features that ReactPy requires.
+
+### Removed
+
+- `settings.py:REACTPY_RECONNECT_MAX` is removed. See the docs for the new `REACTPY_RECONNECT_*` settings.
## [3.4.0] - 2023-08-18
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index 2fc90f77..754b81be 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -165,7 +165,7 @@ def reactpy_warnings(app_configs, **kwargs):
warnings.append(
Warning(
"REACTPY_RECONNECT_MAX has been removed.",
- hint="Use REACTPY_SESSION_MAX_AGE as an alternative.",
+ hint="See the docs for the new REACTPY_RECONNECT_* settings.",
id="reactpy_django.W013",
)
)
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index d8ca67b4..d811f7dc 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -28,11 +28,6 @@
"REACTPY_WEBSOCKET_URL",
"reactpy/",
)
-REACTPY_RECONNECT_MAX: int = getattr(
- settings,
- "REACTPY_RECONNECT_MAX",
- 259200, # Default to 3 days
-)
# Configurable through Django settings.py
REACTPY_URL_PREFIX: str = getattr(
@@ -43,7 +38,7 @@
REACTPY_SESSION_MAX_AGE: int = getattr(
settings,
"REACTPY_SESSION_MAX_AGE",
- REACTPY_RECONNECT_MAX,
+ 259200, # Default to 3 days
)
REACTPY_CACHE: str = getattr(
settings,
diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py
index b486dc3c..e40bc68e 100644
--- a/src/reactpy_django/websocket/consumer.py
+++ b/src/reactpy_django/websocket/consumer.py
@@ -105,7 +105,7 @@ async def disconnect(self, code: int) -> None:
except Exception:
await asyncio.to_thread(
_logger.exception,
- "ReactPy has failed to update component session `last_accessed`!",
+ "ReactPy has failed to save component session!",
)
await super().disconnect(code)
From dcc280ec9d261ff4baf95842d90d7eabba94c5a8 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 24 Aug 2023 00:55:47 -0700
Subject: [PATCH 10/16] fix typos
---
CHANGELOG.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f179f70..671dd500 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,7 @@ Using the following categories, list your changes in this order:
## Added
-- More reconnection behavior customizability through new settings!
+- More customization for reconnection behavior through new settings!
- `REACTPY_RECONNECT_INTERVAL`
- `REACTPY_RECONNECT_MAX_INTERVAL`
- `REACTPY_RECONNECT_MAX_RETRIES`
@@ -48,7 +48,7 @@ Using the following categories, list your changes in this order:
- Prettier websocket URLs for components that do not have sessions.
- Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled.
- Bumped the minimum `@reactpy/client` version to `0.3.1`
-- Use TypeScript instead of JavaScript for this repo.
+- Use TypeScript instead of JavaScript for this repository.
- Bumped minimum Django version to `4.2`.
- Note: Moving forward, ReactPy-Django will only support Django's latest versions to increase async support (and performance). This latest-only trend will continue until Django has all async features that ReactPy requires.
From d4424b9c0870499c2e1e939fa0198cef173ba745 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 26 Aug 2023 14:28:46 -0700
Subject: [PATCH 11/16] fix changelog "added" title
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8a53a2a..1424a0e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,7 +34,7 @@ Using the following categories, list your changes in this order:
## [Unreleased]
-## Added
+### Added
- More customization for reconnection behavior through new settings!
- `REACTPY_RECONNECT_INTERVAL`
From eb37320a54dba47e176259bdefc17e81d3f75c4a Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 26 Aug 2023 19:41:12 -0700
Subject: [PATCH 12/16] minor refactoring
---
src/reactpy_django/templatetags/reactpy.py | 4 +--
src/reactpy_django/types.py | 4 +--
src/reactpy_django/utils.py | 20 ++++++------
src/reactpy_django/websocket/consumer.py | 36 +++++++++++++---------
tests/test_app/tests/test_database.py | 10 +++---
5 files changed, 41 insertions(+), 33 deletions(-)
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index e49b3769..82640ced 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -15,7 +15,7 @@
ComponentParamError,
InvalidHostError,
)
-from reactpy_django.types import ComponentParamData
+from reactpy_django.types import ComponentParams
from reactpy_django.utils import validate_component_args
try:
@@ -129,7 +129,7 @@ def failure_context(dotted_path: str, error: Exception):
def save_component_params(args, kwargs, uuid):
- params = ComponentParamData(args, kwargs)
+ params = ComponentParams(args, kwargs)
model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
model.full_clean()
model.save()
diff --git a/src/reactpy_django/types.py b/src/reactpy_django/types.py
index 02fdec6e..ac6205e0 100644
--- a/src/reactpy_django/types.py
+++ b/src/reactpy_django/types.py
@@ -33,7 +33,7 @@
"SyncPostprocessor",
"QueryOptions",
"MutationOptions",
- "ComponentParamData",
+ "ComponentParams",
]
_Result = TypeVar("_Result", bound=Union[Model, QuerySet[Any]])
@@ -127,7 +127,7 @@ class MutationOptions:
@dataclass
-class ComponentParamData:
+class ComponentParams:
"""Container used for serializing component parameters.
This dataclass is pickled & stored in the database, then unpickled when needed."""
diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py
index f40ea1ee..755b3a05 100644
--- a/src/reactpy_django/utils.py
+++ b/src/reactpy_django/utils.py
@@ -324,9 +324,10 @@ def create_cache_key(*args):
return f"reactpy_django:{':'.join(str(arg) for arg in args)}"
-def db_cleanup(immediate: bool = False):
+def delete_expired_sessions(immediate: bool = False):
"""Deletes expired component sessions from the database.
- This function may be expanded in the future to include additional cleanup tasks."""
+ As a performance optimization, this is only run once every REACTPY_SESSION_MAX_AGE seconds.
+ """
from .config import REACTPY_DEBUG_MODE, REACTPY_SESSION_MAX_AGE
from .models import ComponentSession, Config
@@ -343,10 +344,11 @@ def db_cleanup(immediate: bool = False):
config.save()
# Check if cleaning took abnormally long
- clean_duration = timezone.now() - start_time
- if REACTPY_DEBUG_MODE and clean_duration.total_seconds() > 1:
- _logger.warning(
- "ReactPy has taken %s seconds to clean up expired component sessions. "
- "This may indicate a performance issue with your system, cache, or database.",
- clean_duration.total_seconds(),
- )
+ if REACTPY_DEBUG_MODE:
+ clean_duration = timezone.now() - start_time
+ if clean_duration.total_seconds() > 1:
+ _logger.warning(
+ "ReactPy has taken %s seconds to clean up expired component sessions. "
+ "This may indicate a performance issue with your system, cache, or database.",
+ clean_duration.total_seconds(),
+ )
diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py
index e40bc68e..c6a47c27 100644
--- a/src/reactpy_django/websocket/consumer.py
+++ b/src/reactpy_django/websocket/consumer.py
@@ -21,8 +21,8 @@
from reactpy.core.layout import Layout
from reactpy.core.serve import serve_layout
-from reactpy_django.types import ComponentParamData, ComponentWebsocket
-from reactpy_django.utils import db_cleanup
+from reactpy_django.types import ComponentParams, ComponentWebsocket
+from reactpy_django.utils import delete_expired_sessions
_logger = logging.getLogger(__name__)
backhaul_loop = asyncio.new_event_loop()
@@ -98,8 +98,19 @@ async def disconnect(self, code: int) -> None:
"""The browser has disconnected."""
self.dispatcher.cancel()
- # Update the last_accessed timestamp
if self.component_session:
+ # Clean up expired component sessions
+ try:
+ await database_sync_to_async(
+ delete_expired_sessions, thread_sensitive=False
+ )()
+ except Exception:
+ await asyncio.to_thread(
+ _logger.exception,
+ "ReactPy has failed to delete expired component sessions!",
+ )
+
+ # Update the last_accessed timestamp
try:
await self.component_session.asave()
except Exception:
@@ -149,8 +160,8 @@ async def run_dispatcher(self):
carrier=ComponentWebsocket(self.close, self.disconnect, dotted_path),
)
now = timezone.now()
- component_args: Sequence[Any] = ()
- component_kwargs: MutableMapping[str, Any] = {}
+ component_session_args: Sequence[Any] = ()
+ component_session_kwargs: MutableMapping[str, Any] = {}
# Verify the component has already been registered
try:
@@ -165,23 +176,18 @@ async def run_dispatcher(self):
# Fetch the component's args/kwargs from the database, if needed
try:
if uuid:
- # Always clean up expired entries first
- await database_sync_to_async(db_cleanup, thread_sensitive=False)()
-
# Get the component session from the DB
self.component_session = await models.ComponentSession.objects.aget(
uuid=uuid,
last_accessed__gt=now - timedelta(seconds=REACTPY_SESSION_MAX_AGE),
)
- component_params: ComponentParamData = pickle.loads(
- self.component_session.params
- )
- component_args = component_params.args
- component_kwargs = component_params.kwargs
+ params: ComponentParams = pickle.loads(self.component_session.params)
+ component_session_args = params.args
+ component_session_kwargs = params.kwargs
# Generate the initial component instance
component_instance = component_constructor(
- *component_args, **component_kwargs
+ *component_session_args, **component_session_kwargs
)
except models.ComponentSession.DoesNotExist:
await asyncio.to_thread(
@@ -196,7 +202,7 @@ async def run_dispatcher(self):
await asyncio.to_thread(
_logger.exception,
f"Failed to construct component {component_constructor} "
- f"with args='{component_args}' kwargs='{component_kwargs}'!",
+ f"with args='{component_session_args}' kwargs='{component_session_kwargs}'!",
)
return
diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py
index 94f75950..0c9a1b84 100644
--- a/tests/test_app/tests/test_database.py
+++ b/tests/test_app/tests/test_database.py
@@ -6,7 +6,7 @@
from django.test import TransactionTestCase
from reactpy_django import utils
from reactpy_django.models import ComponentSession
-from reactpy_django.types import ComponentParamData
+from reactpy_django.types import ComponentParams
class RoutedDatabaseTests(TransactionTestCase):
@@ -15,7 +15,7 @@ class RoutedDatabaseTests(TransactionTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- utils.db_cleanup(immediate=True)
+ utils.delete_expired_sessions(immediate=True)
def test_component_params(self):
# Make sure the ComponentParams table is empty
@@ -39,7 +39,7 @@ def test_component_params(self):
self.assertEqual(ComponentSession.objects.count(), 2)
# Delete the first component params based on expiration time
- utils.db_cleanup() # Don't use `immediate` to test cache timestamping logic
+ utils.delete_expired_sessions() # Don't use `immediate` to test timestamping logic
# Make sure `params_1` has expired
self.assertEqual(ComponentSession.objects.count(), 1)
@@ -47,9 +47,9 @@ def test_component_params(self):
pickle.loads(ComponentSession.objects.first().params), params_2 # type: ignore
)
- def _save_params_to_db(self, value: Any) -> ComponentParamData:
+ def _save_params_to_db(self, value: Any) -> ComponentParams:
db = list(self.databases)[0]
- param_data = ComponentParamData((value,), {"test_value": value})
+ param_data = ComponentParams((value,), {"test_value": value})
model = ComponentSession(uuid4().hex, params=pickle.dumps(param_data))
model.clean_fields()
model.clean()
From 88464c4995a424fa87d9b65ccf7889abd5453e01 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 26 Aug 2023 19:59:04 -0700
Subject: [PATCH 13/16] remove mike set-default
---
.github/workflows/publish-release-docs.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml
index b58cb0ed..a0c8861d 100644
--- a/.github/workflows/publish-release-docs.yml
+++ b/.github/workflows/publish-release-docs.yml
@@ -20,4 +20,3 @@ jobs:
git config user.name github-actions
git config user.email github-actions@github.com
mike deploy --push --update-aliases ${{ github.event.release.name }} latest
- mike set-default --push latest
From 86fdbbea3f50401f6f962bfbb4bde4efb8fc22d1 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 26 Aug 2023 20:09:52 -0700
Subject: [PATCH 14/16] tweak changelog
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1424a0e5..da060bbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,7 +51,7 @@ Using the following categories, list your changes in this order:
- Bumped the minimum `@reactpy/client` version to `0.3.1`
- Use TypeScript instead of JavaScript for this repository.
- Bumped minimum Django version to `4.2`.
- - Note: Moving forward, ReactPy-Django will only support Django's latest versions to increase async support (and performance). This latest-only trend will continue until Django has all async features that ReactPy requires.
+ - Note: ReactPy-Django will continue bumping minimum Django requirements to versions that increase async support. This "latest-only" trend will continue until Django has all async features that ReactPy benefits from. After this point, ReactPy-Django will begin supporting all maintained Django versions.
### Removed
From 3b8f0feefb3bb4a74a6255d9fb165fdbb3236d1f Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 26 Aug 2023 20:20:19 -0700
Subject: [PATCH 15/16] bump node and npm
---
.github/workflows/publish-py.yml | 56 ++++++++++++++++----------------
.github/workflows/test-src.yml | 2 +-
2 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-py.yml
index 4439cb5e..09be8866 100644
--- a/.github/workflows/publish-py.yml
+++ b/.github/workflows/publish-py.yml
@@ -4,33 +4,33 @@
name: Publish Python
on:
- release:
- types: [published]
+ release:
+ types: [published]
jobs:
- release-package:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
- with:
- node-version: "14.x"
- - name: Set up Python
- uses: actions/setup-python@v1
- with:
- python-version: "3.x"
- - name: Install NPM
- run: |
- npm install -g npm@7.22.0
- npm --version
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements/build-pkg.txt
- - name: Build and publish
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- run: |
- python -m build --sdist --wheel --outdir dist .
- twine upload dist/*
+ release-package:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: "20.x"
+ - name: Set up Python
+ uses: actions/setup-python@v1
+ with:
+ python-version: "3.x"
+ - name: Install NPM
+ run: |
+ npm install -g npm@latest
+ npm --version
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements/build-pkg.txt
+ - name: Build and publish
+ env:
+ TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+ run: |
+ python -m build --sdist --wheel --outdir dist .
+ twine upload dist/*
diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml
index f945ea9d..4a03c49c 100644
--- a/.github/workflows/test-src.yml
+++ b/.github/workflows/test-src.yml
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: "14.x"
+ node-version: "20.x"
- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
From 69aee026bc0c86afd6d131edeb0178b99bc28403 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 26 Aug 2023 20:31:37 -0700
Subject: [PATCH 16/16] add hyperlinks to localhost
---
docs/src/contribute/code.md | 2 +-
docs/src/contribute/docs.md | 2 +-
docs/src/get-started/run-webserver.md | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/src/contribute/code.md b/docs/src/contribute/code.md
index cc5b8e58..a5b52955 100644
--- a/docs/src/contribute/code.md
+++ b/docs/src/contribute/code.md
@@ -45,7 +45,7 @@ cd tests
python manage.py runserver
```
-Navigate to `http://127.0.0.1:8000` to see if the tests are rendering correctly.
+Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to see if the tests are rendering correctly.
## GitHub Pull Request
diff --git a/docs/src/contribute/docs.md b/docs/src/contribute/docs.md
index 33b83fb7..913bc0a6 100644
--- a/docs/src/contribute/docs.md
+++ b/docs/src/contribute/docs.md
@@ -37,7 +37,7 @@ Finally, to verify that everything is working properly, you can manually run the
mkdocs serve
```
-Navigate to `http://127.0.0.1:8000` to view a preview of the documentation.
+Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to view a preview of the documentation.
## GitHub Pull Request
diff --git a/docs/src/get-started/run-webserver.md b/docs/src/get-started/run-webserver.md
index cefeafc3..cb4f87f1 100644
--- a/docs/src/get-started/run-webserver.md
+++ b/docs/src/get-started/run-webserver.md
@@ -16,7 +16,7 @@ To test your new Django view, run the following command to start up a developmen
python manage.py runserver
```
-Now you can navigate to your **Django project** URL that contains a ReactPy component, such as `http://127.0.0.1:8000/example/` ([_from the previous step_](./register-view.md)).
+Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](./register-view.md)).
If you copy-pasted our example component, you will now see your component display "Hello World".