tina4-python 3.10.93__tar.gz → 3.10.95__tar.gz
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.
- {tina4_python-3.10.93 → tina4_python-3.10.95}/PKG-INFO +1 -1
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/CLAUDE.md +22 -10
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/__init__.py +1 -1
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/server.py +134 -27
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dev_reload.py +1 -1
- tina4_python-3.10.95/tina4_python/public/js/tina4js.min.js +48 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/websocket/__init__.py +30 -1
- tina4_python-3.10.93/tina4_python/public/js/tina4js.min.js +0 -47
- {tina4_python-3.10.93 → tina4_python-3.10.95}/.gitignore +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/README.md +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/pyproject.toml +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/request.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/router.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -362,6 +362,8 @@ tina4_python/ # Core framework package (v3.0.0)
|
|
|
362
362
|
|
|
363
363
|
## Starting the Server
|
|
364
364
|
|
|
365
|
+
**IMPORTANT:** Always use `tina4 serve` to start Tina4 projects. Never run `python app.py` directly — the framework will refuse to start without the tina4 CLI. The CLI handles SCSS compilation, file watching, and server lifecycle.
|
|
366
|
+
|
|
365
367
|
```python
|
|
366
368
|
# app.py
|
|
367
369
|
from tina4_python.core import run
|
|
@@ -372,19 +374,28 @@ if __name__ == "__main__":
|
|
|
372
374
|
|
|
373
375
|
`run()` automatically discovers and imports all Python files in `src/` — no manual imports needed. Route decorators (`@get`, `@post`, etc.) register themselves on import. Configure host/port via environment variables or `resolve_config()`.
|
|
374
376
|
|
|
377
|
+
### Running the Server
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
tina4 serve # Start dev server (SCSS watch + live reload)
|
|
381
|
+
tina4 serve --production # Start with production server (auto-installs uvicorn)
|
|
382
|
+
tina4 serve --no-browser # Don't open browser on startup
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
To run without the tina4 CLI (e.g. in Docker or CI), add `TINA4_OVERRIDE_CLIENT=true` to your `.env` file.
|
|
386
|
+
|
|
375
387
|
### Package Manager
|
|
376
388
|
|
|
377
389
|
```bash
|
|
378
390
|
uv add tina4-python # Add dependency
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
uv run tina4python generate middleware <n> # Generate middleware scaffold
|
|
391
|
+
tina4 serve # Start dev server
|
|
392
|
+
tina4 serve --production # Production server
|
|
393
|
+
tina4 init python . # Scaffold project structure
|
|
394
|
+
tina4 migrate # Run pending SQL migrations
|
|
395
|
+
tina4 generate model <name> # Generate ORM model scaffold
|
|
396
|
+
tina4 generate route <name> # Generate route scaffold
|
|
397
|
+
tina4 generate migration <desc> # Generate migration file
|
|
398
|
+
tina4 generate middleware <name> # Generate middleware scaffold
|
|
388
399
|
```
|
|
389
400
|
|
|
390
401
|
## Development Mode (DevReload)
|
|
@@ -1560,6 +1571,7 @@ TINA4_DEBUG=true # Enable dev mode (toolbar, live reload, error
|
|
|
1560
1571
|
TINA4_LOG_LEVEL=ERROR # Log verbosity: ALL, DEBUG, INFO, WARNING, ERROR (default: ERROR)
|
|
1561
1572
|
TINA4_LOCALE=en # Language for framework messages (en, fr, af, zh, ja, es)
|
|
1562
1573
|
TINA4_DEFAULT_WEBSERVER=FALSE # Set to TRUE to use Tina4's built-in webserver instead of ASGI
|
|
1574
|
+
TINA4_OVERRIDE_CLIENT=false # Set to true to allow running without tina4 CLI (e.g. Docker)
|
|
1563
1575
|
HOST_NAME=localhost:7145
|
|
1564
1576
|
|
|
1565
1577
|
# Sessions
|
|
@@ -1791,7 +1803,7 @@ async def dashboard(request, response):
|
|
|
1791
1803
|
## v3 Features Summary
|
|
1792
1804
|
|
|
1793
1805
|
- **55 built-in features**, zero third-party dependencies
|
|
1794
|
-
- **2,
|
|
1806
|
+
- **2,299 tests** passing across all modules
|
|
1795
1807
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1796
1808
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1797
1809
|
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
|
|
@@ -485,20 +485,26 @@ async def _handle_asgi_websocket(scope: dict, receive, send):
|
|
|
485
485
|
conn = _AsgiWebSocketConnection(scope, receive, send, path, params, _ws_manager)
|
|
486
486
|
_ws_manager.add(conn)
|
|
487
487
|
|
|
488
|
-
# Fire "open" event
|
|
488
|
+
# Fire "open" event — this may set conn._on_message / conn._on_close
|
|
489
|
+
# via WebSocketServer's decorator-style handler
|
|
489
490
|
try:
|
|
490
491
|
await handler(conn, "open", None)
|
|
491
492
|
except Exception as e:
|
|
492
493
|
Log.error(f"WebSocket open handler error: {e}")
|
|
493
494
|
|
|
494
|
-
# Message loop
|
|
495
|
+
# Message loop — prefer decorator-style handlers if set during open
|
|
495
496
|
try:
|
|
496
497
|
while True:
|
|
497
498
|
msg = await receive()
|
|
498
499
|
if msg["type"] == "websocket.receive":
|
|
499
500
|
data = msg.get("text") or (msg.get("bytes", b"").decode("utf-8", errors="replace") if msg.get("bytes") else "")
|
|
500
501
|
try:
|
|
501
|
-
|
|
502
|
+
if conn._on_message:
|
|
503
|
+
result = conn._on_message(data)
|
|
504
|
+
if asyncio.iscoroutine(result):
|
|
505
|
+
await result
|
|
506
|
+
else:
|
|
507
|
+
await handler(conn, "message", data)
|
|
502
508
|
except Exception as e:
|
|
503
509
|
Log.error(f"WebSocket message handler error: {e}")
|
|
504
510
|
elif msg["type"] == "websocket.disconnect":
|
|
@@ -506,16 +512,29 @@ async def _handle_asgi_websocket(scope: dict, receive, send):
|
|
|
506
512
|
except Exception:
|
|
507
513
|
pass
|
|
508
514
|
finally:
|
|
509
|
-
# Fire "close" event
|
|
515
|
+
# Fire "close" event — prefer decorator-style if set
|
|
510
516
|
try:
|
|
511
|
-
|
|
517
|
+
if conn._on_close:
|
|
518
|
+
result = conn._on_close()
|
|
519
|
+
if asyncio.iscoroutine(result):
|
|
520
|
+
await result
|
|
521
|
+
else:
|
|
522
|
+
await handler(conn, "close", None)
|
|
512
523
|
except Exception as e:
|
|
513
524
|
Log.error(f"WebSocket close handler error: {e}")
|
|
525
|
+
# Clean up rooms
|
|
526
|
+
for room_name in list(conn._rooms):
|
|
527
|
+
_ws_manager._leave_room(conn.id, room_name)
|
|
528
|
+
conn._rooms.clear()
|
|
514
529
|
_ws_manager.remove(conn)
|
|
515
530
|
|
|
516
531
|
|
|
517
532
|
class _AsgiWebSocketConnection:
|
|
518
|
-
"""WebSocket connection wrapper for ASGI servers (uvicorn, etc.).
|
|
533
|
+
"""WebSocket connection wrapper for ASGI servers (uvicorn, etc.).
|
|
534
|
+
|
|
535
|
+
Supports both Router's (conn, event, data) style and WebSocketServer's
|
|
536
|
+
decorator style (@conn.on_message / @conn.on_close).
|
|
537
|
+
"""
|
|
519
538
|
|
|
520
539
|
def __init__(self, scope, receive, send, path, params, manager):
|
|
521
540
|
self.id = str(uuid.uuid4())[:8]
|
|
@@ -530,6 +549,10 @@ class _AsgiWebSocketConnection:
|
|
|
530
549
|
self._send = send
|
|
531
550
|
self._manager = manager
|
|
532
551
|
self._closed = False
|
|
552
|
+
self._on_message = None
|
|
553
|
+
self._on_close = None
|
|
554
|
+
self._on_error = None
|
|
555
|
+
self._rooms: set = set()
|
|
533
556
|
|
|
534
557
|
client = scope.get("client", ("unknown", 0))
|
|
535
558
|
self.ip = client[0] if client else "unknown"
|
|
@@ -540,6 +563,42 @@ class _AsgiWebSocketConnection:
|
|
|
540
563
|
def closed(self) -> bool:
|
|
541
564
|
return self._closed
|
|
542
565
|
|
|
566
|
+
@property
|
|
567
|
+
def rooms(self) -> set:
|
|
568
|
+
"""Return the set of room names this connection has joined."""
|
|
569
|
+
return self._rooms
|
|
570
|
+
|
|
571
|
+
def on_message(self, handler):
|
|
572
|
+
"""Register a message handler (decorator style)."""
|
|
573
|
+
self._on_message = handler
|
|
574
|
+
|
|
575
|
+
def on_close(self, handler):
|
|
576
|
+
"""Register a close handler (decorator style)."""
|
|
577
|
+
self._on_close = handler
|
|
578
|
+
|
|
579
|
+
def on_error(self, handler):
|
|
580
|
+
"""Register an error handler (decorator style)."""
|
|
581
|
+
self._on_error = handler
|
|
582
|
+
|
|
583
|
+
def join_room(self, room_name: str) -> None:
|
|
584
|
+
"""Join a named room."""
|
|
585
|
+
self._rooms.add(room_name)
|
|
586
|
+
if self._manager:
|
|
587
|
+
self._manager._join_room(self.id, room_name)
|
|
588
|
+
|
|
589
|
+
def leave_room(self, room_name: str) -> None:
|
|
590
|
+
"""Leave a named room."""
|
|
591
|
+
self._rooms.discard(room_name)
|
|
592
|
+
if self._manager:
|
|
593
|
+
self._manager._leave_room(self.id, room_name)
|
|
594
|
+
|
|
595
|
+
async def broadcast_to_room(self, room_name: str, message: str | bytes,
|
|
596
|
+
exclude_self: bool = False) -> None:
|
|
597
|
+
"""Broadcast a message to all connections in a room."""
|
|
598
|
+
if self._manager:
|
|
599
|
+
exclude = self.id if exclude_self else None
|
|
600
|
+
await self._manager.broadcast_to_room(room_name, message, exclude=exclude)
|
|
601
|
+
|
|
543
602
|
async def send(self, message: str | bytes):
|
|
544
603
|
"""Send a text or binary message."""
|
|
545
604
|
if self._closed:
|
|
@@ -611,35 +670,51 @@ async def _handle_dev_websocket(reader, writer, headers, path):
|
|
|
611
670
|
|
|
612
671
|
handler = route["handler"]
|
|
613
672
|
|
|
614
|
-
# Fire "open" event
|
|
673
|
+
# Fire "open" event — this may set ws._on_message / ws._on_close
|
|
674
|
+
# via WebSocketServer's decorator-style handler
|
|
615
675
|
try:
|
|
616
676
|
await handler(ws, "open", None)
|
|
617
677
|
except Exception as e:
|
|
618
678
|
Log.error(f"WebSocket open handler error: {e}")
|
|
619
679
|
|
|
620
|
-
#
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
except Exception as e:
|
|
625
|
-
Log.error(f"WebSocket message handler error: {e}")
|
|
680
|
+
# If the open handler set decorator-style callbacks, use those directly.
|
|
681
|
+
# Otherwise fall back to calling handler(ws, "message/close", data).
|
|
682
|
+
decorator_on_message = ws._on_message
|
|
683
|
+
decorator_on_close = ws._on_close
|
|
626
684
|
|
|
627
|
-
|
|
685
|
+
if not decorator_on_message:
|
|
686
|
+
async def on_message(message):
|
|
687
|
+
try:
|
|
688
|
+
await handler(ws, "message", message)
|
|
689
|
+
except Exception as e:
|
|
690
|
+
Log.error(f"WebSocket message handler error: {e}")
|
|
691
|
+
ws._on_message = on_message
|
|
628
692
|
|
|
629
|
-
|
|
693
|
+
if not decorator_on_close:
|
|
694
|
+
original_on_close = ws._on_close
|
|
630
695
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if original_on_close:
|
|
638
|
-
result = original_on_close()
|
|
639
|
-
if asyncio.iscoroutine(result):
|
|
640
|
-
await result
|
|
696
|
+
async def on_close():
|
|
697
|
+
try:
|
|
698
|
+
await handler(ws, "close", None)
|
|
699
|
+
except Exception as e:
|
|
700
|
+
Log.error(f"WebSocket close handler error: {e}")
|
|
701
|
+
_ws_manager.remove(ws)
|
|
641
702
|
|
|
642
|
-
|
|
703
|
+
ws._on_close = on_close
|
|
704
|
+
else:
|
|
705
|
+
# Wrap the decorator close handler to also clean up the manager
|
|
706
|
+
_user_on_close = decorator_on_close
|
|
707
|
+
|
|
708
|
+
async def on_close_with_cleanup():
|
|
709
|
+
try:
|
|
710
|
+
result = _user_on_close()
|
|
711
|
+
if asyncio.iscoroutine(result):
|
|
712
|
+
await result
|
|
713
|
+
except Exception as e:
|
|
714
|
+
Log.error(f"WebSocket close handler error: {e}")
|
|
715
|
+
_ws_manager.remove(ws)
|
|
716
|
+
|
|
717
|
+
ws._on_close = on_close_with_cleanup
|
|
643
718
|
|
|
644
719
|
# Enter the frame loop
|
|
645
720
|
await ws._run()
|
|
@@ -648,7 +723,12 @@ async def _handle_dev_websocket(reader, writer, headers, path):
|
|
|
648
723
|
if not ws._closed:
|
|
649
724
|
ws._closed = True
|
|
650
725
|
try:
|
|
651
|
-
|
|
726
|
+
if decorator_on_close:
|
|
727
|
+
result = decorator_on_close()
|
|
728
|
+
if asyncio.iscoroutine(result):
|
|
729
|
+
await result
|
|
730
|
+
else:
|
|
731
|
+
await handler(ws, "close", None)
|
|
652
732
|
except Exception:
|
|
653
733
|
pass
|
|
654
734
|
_ws_manager.remove(ws)
|
|
@@ -1468,6 +1548,33 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1468
1548
|
global _start_time
|
|
1469
1549
|
_start_time = time.time()
|
|
1470
1550
|
|
|
1551
|
+
# ── Require tina4 CLI ─────────────────────────────────────────
|
|
1552
|
+
# The framework must be launched via `tina4 serve`, not `python app.py`.
|
|
1553
|
+
# The tina4 CLI sets TINA4_CLI=true when spawning the server process.
|
|
1554
|
+
# Users can bypass this by adding TINA4_OVERRIDE_CLIENT=true to .env
|
|
1555
|
+
if os.environ.get("TINA4_CLI") != "true" and os.environ.get("TINA4_OVERRIDE_CLIENT") != "true":
|
|
1556
|
+
# Load .env early so TINA4_OVERRIDE_CLIENT can be read
|
|
1557
|
+
from tina4_python.dotenv import load_env
|
|
1558
|
+
load_env()
|
|
1559
|
+
if os.environ.get("TINA4_OVERRIDE_CLIENT") != "true":
|
|
1560
|
+
print()
|
|
1561
|
+
print("=" * 60)
|
|
1562
|
+
print()
|
|
1563
|
+
print(" Tina4 must be started with the tina4 CLI:")
|
|
1564
|
+
print()
|
|
1565
|
+
print(" tina4 serve (development)")
|
|
1566
|
+
print(" tina4 serve --production (production)")
|
|
1567
|
+
print()
|
|
1568
|
+
print(" Install: cargo install tina4")
|
|
1569
|
+
print(" Docs: https://tina4.com")
|
|
1570
|
+
print()
|
|
1571
|
+
print(" To run directly, add to .env:")
|
|
1572
|
+
print(" TINA4_OVERRIDE_CLIENT=true")
|
|
1573
|
+
print()
|
|
1574
|
+
print("=" * 60)
|
|
1575
|
+
print()
|
|
1576
|
+
sys.exit(1)
|
|
1577
|
+
|
|
1471
1578
|
if no_reload:
|
|
1472
1579
|
os.environ["TINA4_NO_RELOAD"] = "true"
|
|
1473
1580
|
|
|
@@ -168,7 +168,7 @@ def _poll_loop(directories: list[str], interval: float = 1.0):
|
|
|
168
168
|
except Exception as e:
|
|
169
169
|
Log.error(f"DevReload: route re-discovery failed: {e}")
|
|
170
170
|
|
|
171
|
-
# Note: SCSS compilation is handled by the
|
|
171
|
+
# Note: SCSS compilation is handled by the tina4 CLI (Rust).
|
|
172
172
|
# DevReload only handles route re-discovery and browser refresh.
|
|
173
173
|
|
|
174
174
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";var Tina4=(()=>{var B=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var qe=Object.getOwnPropertyNames;var Ne=Object.prototype.hasOwnProperty;var Oe=(t,n)=>{for(var e in n)B(t,e,{get:n[e],enumerable:!0})},Ie=(t,n,e,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of qe(n))!Ne.call(t,r)&&r!==e&&B(t,r,{get:()=>n[r],enumerable:!(o=Me(n,r))||o.enumerable});return t};var De=t=>Ie(B({},"__esModule",{value:!0}),t);var Ve={};Oe(Ve,{Tina4Element:()=>W,api:()=>we,batch:()=>F,computed:()=>ce,effect:()=>b,html:()=>ue,isSignal:()=>R,navigate:()=>G,pwa:()=>Se,route:()=>me,router:()=>ve,signal:()=>v,sse:()=>Te,ws:()=>Ee});var q=null,I=null,j=null;function S(t){j=t}function U(){return j}var ae=null,ie=null;var K=0,z=new Set;function v(t,n){let e=t,o=new Set,r={_t4:!0,get value(){if(q&&(o.add(q),I)){let s=q;I.push(()=>o.delete(s))}return e},set value(s){if(Object.is(s,e))return;let i=e;if(e=s,r._debugInfo&&r._debugInfo.updateCount++,ie&&ie(r,i,s),K>0)for(let a of o)z.add(a);else{let a;for(let c of[...o])try{c()}catch(l){a===void 0&&(a=l)}if(a!==void 0)throw a}},_subscribe(s){return o.add(s),()=>{o.delete(s)}},peek(){return e}};return ae&&(r._debugInfo={label:n,createdAt:Date.now(),updateCount:0,subs:o},ae(r,n)),r}function ce(t){let n=v(void 0);return b(()=>{n.value=t()}),{_t4:!0,get value(){return n.value},set value(e){throw new Error("[tina4] computed signals are read-only")},_subscribe(e){return n._subscribe(e)},peek(){return n.peek()}}}function b(t){let n=!1,e=[],o=()=>{if(n)return;for(let a of e)a();e=[];let s=q,i=I;q=o,I=e;try{t()}finally{q=s,I=i}};o();let r=()=>{n=!0;for(let s of e)s();e=[]};return j&&j.push(r),r}function F(t){K++;try{t()}finally{if(K--,K===0){let n=[...z];z.clear();let e;for(let o of n)try{o()}catch(r){e===void 0&&(e=r)}if(e!==void 0)throw e}}}function R(t){return t!==null&&typeof t=="object"&&t._t4===!0}var le=new WeakMap,Q="t4:";function ue(t,...n){let e=le.get(t);if(!e){e=document.createElement("template");let i="";for(let a=0;a<t.length;a++)i+=t[a],a<n.length&&(je(i)?i+=`__t4_${a}__`:i+=`<!--${Q}${a}-->`);e.innerHTML=i,le.set(t,e)}let o=e.content.cloneNode(!0),r=He(o);for(let{marker:i,index:a}of r)Pe(i,n[a]);let s=Le(o);for(let i of s)Ke(i,n);return o}function He(t){let n=[];return Y(t,e=>{if(e.nodeType===8){let o=e.data;if(o&&o.startsWith(Q)){let r=parseInt(o.slice(Q.length),10);n.push({marker:e,index:r})}}}),n}function Le(t){let n=[];return Y(t,e=>{e.nodeType===1&&n.push(e)}),n}function Y(t,n){let e=t.childNodes;for(let o=0;o<e.length;o++){let r=e[o];n(r),Y(r,n)}}function Pe(t,n){let e=t.parentNode;if(e)if(R(n)){let o=document.createTextNode("");e.replaceChild(o,t),b(()=>{o.data=String(n.value??"")})}else if(typeof n=="function"){let o=document.createComment("");e.replaceChild(o,t);let r=[],s=[];b(()=>{for(let u of s)u();s=[];let i=[],a=U();S(i);let c=n();S(a),s=i;for(let u of r)u.parentNode?.removeChild(u);r=[];let l=X(c),d=o.parentNode;if(d)for(let u of l)d.insertBefore(u,o),r.push(u)})}else if(de(n))e.replaceChild(n,t);else if(n instanceof Node)e.replaceChild(n,t);else if(Array.isArray(n)){let o=document.createDocumentFragment();for(let r of n){let s=X(r);for(let i of s)o.appendChild(i)}e.replaceChild(o,t)}else{let o=document.createTextNode(String(n??""));e.replaceChild(o,t)}}function Ke(t,n){let e=[];for(let o of Array.from(t.attributes)){let r=o.name,s=o.value;if(r.startsWith("@")){let a=r.slice(1),c=s.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];typeof l=="function"&&t.addEventListener(a,d=>F(()=>l(d)))}e.push(r);continue}if(r.startsWith("?")){let a=r.slice(1),c=s.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];if(R(l)){let d=l;b(()=>{d.value?t.setAttribute(a,""):t.removeAttribute(a)})}else typeof l=="function"?b(()=>{l()?t.setAttribute(a,""):t.removeAttribute(a)}):l&&t.setAttribute(a,"")}e.push(r);continue}if(r.startsWith(".")){let a=r.slice(1),c=s.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];R(l)?b(()=>{t[a]=l.value}):t[a]=l}e.push(r);continue}let i=s.match(/__t4_(\d+)__/);if(i){let a=n[parseInt(i[1],10)];if(R(a)){let c=a;b(()=>{t.setAttribute(r,String(c.value??""))})}else typeof a=="function"?b(()=>{t.setAttribute(r,String(a()??""))}):t.setAttribute(r,String(a??""))}}for(let o of e)t.removeAttribute(o)}function X(t){if(t==null||t===!1)return[];if(de(t))return Array.from(t.childNodes);if(t instanceof Node)return[t];if(Array.isArray(t)){let n=[];for(let e of t)n.push(...X(e));return n}return[document.createTextNode(String(t))]}function de(t){return t!=null&&typeof t=="object"&&t.nodeType===11}function je(t){let n=!1,e=!1,o=!1;for(let r=0;r<t.length;r++){let s=t[r];s==="<"&&!n&&!e&&(o=!0),s===">"&&!n&&!e&&(o=!1),o&&(s==='"'&&!n&&(e=!e),s==="'"&&!e&&(n=!n))}return o}var fe=null,pe=null;var W=class extends HTMLElement{constructor(){super();this._props={};this._rendered=!1;this._disposeRender=null;this._innerDisposers=[];let e=this.constructor;this._root=e.shadow?this.attachShadow({mode:"open"}):this;for(let[o,r]of Object.entries(e.props))this._props[o]=v(this._coerce(this.getAttribute(o),r))}static{this.props={}}static{this.styles=""}static{this.shadow=!0}static get observedAttributes(){return Object.keys(this.props)}connectedCallback(){if(this._rendered)return;this._rendered=!0;let e=this.constructor,o=null;if(e.styles&&e.shadow&&this._root instanceof ShadowRoot){let r=document.createElement("style");r.textContent=e.styles,this._root.appendChild(r),o=r}this._disposeRender=b(()=>{for(let c of this._innerDisposers)c();this._innerDisposers=[];let r=[],s=U();S(r);let i=this.render();S(s),this._innerDisposers=r;let a=Array.from(this._root.childNodes);for(let c of a)c!==o&&this._root.removeChild(c);i&&this._root.appendChild(i)}),this.onMount(),fe&&fe(this)}disconnectedCallback(){this._disposeRender&&(this._disposeRender(),this._disposeRender=null);for(let e of this._innerDisposers)e();this._innerDisposers=[],this.onUnmount(),pe&&pe(this)}attributeChangedCallback(e,o,r){let i=this.constructor.props[e];i&&this._props[e]&&(this._props[e].value=this._coerce(r,i))}prop(e){if(!this._props[e])throw new Error(`[tina4] Prop '${e}' not declared in static props of <${this.tagName.toLowerCase()}>`);return this._props[e]}emit(e,o){this.dispatchEvent(new CustomEvent(e,{bubbles:!0,composed:!0,...o}))}onMount(){}onUnmount(){}_coerce(e,o){return o===Boolean?e!==null:o===Number?e!==null?Number(e):0:e??""}};var Z=[],N=null,D="history",Ue=!1,H=[],$=[],ge=0;function me(t,n){let e=[],o;t==="*"?o=".*":o=t.replace(/\{(\w+)\}/g,(s,i)=>(e.push(i),"([^/]+)"));let r=new RegExp(`^${o}$`);typeof n=="function"?Z.push({pattern:t,regex:r,paramNames:e,handler:n}):Z.push({pattern:t,regex:r,paramNames:e,handler:n.handler,guard:n.guard})}function G(t,n){if(D==="hash")if(n?.replace){let e=new URL(location.href);e.hash="#"+t,history.replaceState(null,"",e.toString()),L()}else location.hash="#"+t;else n?.replace?history.replaceState(null,"",t):history.pushState(null,"",t),L()}function L(){if(!N)return;let t=performance.now(),n=++ge,e=D==="hash"?location.hash.slice(1)||"/":location.pathname;for(let o of Z){let r=e.match(o.regex);if(!r)continue;let s={};if(o.paramNames.forEach((c,l)=>{s[c]=decodeURIComponent(r[l+1])}),o.guard){let c=o.guard();if(c===!1)return;if(typeof c=="string"){G(c,{replace:!0});return}}for(let c of $)c();$=[],N.innerHTML="";let i=[];S(i);let a=o.handler(s);if(a instanceof Promise)a.then(c=>{if(S(null),n!==ge){for(let d of i)d();return}he(N,c),$=i;let l=performance.now()-t;for(let d of H)d({path:e,params:s,pattern:o.pattern,durationMs:l})});else{S(null),he(N,a),$=i;let c=performance.now()-t;for(let l of H)l({path:e,params:s,pattern:o.pattern,durationMs:c})}return}}function he(t,n){n instanceof DocumentFragment||n instanceof Node?t.replaceChildren(n):typeof n=="string"?t.innerHTML=n:n!=null&&t.replaceChildren(document.createTextNode(String(n)))}var ve={start(t){if(N=document.querySelector(t.target),!N)throw new Error(`[tina4] Router target '${t.target}' not found in DOM`);D=t.mode??"history",Ue=!0,window.addEventListener("popstate",L),D==="hash"&&window.addEventListener("hashchange",L),document.addEventListener("click",n=>{if(n.metaKey||n.ctrlKey||n.shiftKey||n.altKey)return;let e=n.target.closest("a[href]");if(!e||e.origin!==location.origin||e.hasAttribute("target")||e.hasAttribute("download")||e.getAttribute("rel")?.includes("external"))return;n.preventDefault();let o=D==="hash"?e.getAttribute("href"):e.pathname;G(o)}),L()},on(t,n){return H.push(n),()=>{let e=H.indexOf(n);e>=0&&H.splice(e,1)}}};var y={baseUrl:"",auth:!1,tokenKey:"tina4_token",headers:{}},J=[],V=[],ye=0;function ee(){try{return localStorage.getItem(y.tokenKey)}catch{return null}}function be(t){try{localStorage.setItem(y.tokenKey,t)}catch{}}async function O(t,n,e,o){let r={method:t,headers:{"Content-Type":"application/json",...y.headers}};if(y.auth){let u=ee();u&&(r.headers.Authorization=`Bearer ${u}`)}if(e!==void 0&&t!=="GET"){let u=typeof e=="object"&&e!==null?{...e}:e;if(y.auth&&typeof u=="object"&&u!==null){let g=ee();g&&(u.formToken=g)}r.body=JSON.stringify(u)}if(o?.headers&&Object.assign(r.headers,o.headers),o?.params){let u=Object.entries(o.params).map(([g,w])=>`${encodeURIComponent(g)}=${encodeURIComponent(String(w))}`).join("&");n+=(n.includes("?")?"&":"?")+u}let s=y.baseUrl+n;r._url=s,r._requestId=++ye;for(let u of J){let g=u(r);g&&(r=g)}let i=await fetch(s,r),a=i.headers.get("FreshToken");a&&be(a);let c=i.headers.get("Content-Type")??"",l;c.includes("json")?l=await i.json():l=await i.text();let d={status:i.status,data:l,ok:i.ok,headers:i.headers,_requestId:r._requestId};for(let u of V){let g=u(d);g&&(d=g)}if(!i.ok)throw d;return d.data}var we={configure(t){Object.assign(y,t)},get(t,n){return O("GET",t,void 0,n)},post(t,n,e){return O("POST",t,n,e)},put(t,n,e){return O("PUT",t,n,e)},patch(t,n,e){return O("PATCH",t,n,e)},delete(t,n){return O("DELETE",t,void 0,n)},async graphql(t,n,e,o){return O("POST",t,{query:n,variables:e||{}},o)},async upload(t,n,e){let o={method:"POST",headers:{...y.headers},body:n};if(delete o.headers["Content-Type"],delete o.headers["content-type"],y.auth){let d=ee();d&&(o.headers.Authorization=`Bearer ${d}`)}if(e?.headers&&Object.assign(o.headers,e.headers),e?.params){let d=Object.entries(e.params).map(([u,g])=>`${encodeURIComponent(u)}=${encodeURIComponent(String(g))}`).join("&");t+=(t.includes("?")?"&":"?")+d}let r=y.baseUrl+t;o._url=r,o._requestId=++ye;for(let d of J){let u=d(o);u&&(o=u)}let s=await fetch(r,o),i=s.headers.get("FreshToken");i&&be(i);let a=s.headers.get("Content-Type")??"",c;a.includes("json")?c=await s.json():c=await s.text();let l={status:s.status,data:c,ok:s.ok,headers:s.headers,_requestId:o._requestId};for(let d of V){let u=d(l);u&&(l=u)}if(!s.ok)throw l;return l.data},intercept(t,n){t==="request"?J.push(n):V.push(n)},_reset(){y.baseUrl="",y.auth=!1,y.tokenKey="tina4_token",y.headers={},J.length=0,V.length=0}};function Fe(t){let n=t.cacheStrategy??"network-first",e=JSON.stringify(t.precache??[]),o=t.offlineRoute?`'${t.offlineRoute}'`:"null";return`
|
|
2
|
+
const CACHE = 'tina4-v1';
|
|
3
|
+
const PRECACHE = ${e};
|
|
4
|
+
const OFFLINE = ${o};
|
|
5
|
+
|
|
6
|
+
self.addEventListener('install', (e) => {
|
|
7
|
+
e.waitUntil(
|
|
8
|
+
caches.open(CACHE).then((c) => c.addAll(PRECACHE)).then(() => self.skipWaiting())
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
self.addEventListener('activate', (e) => {
|
|
13
|
+
e.waitUntil(self.clients.claim());
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
self.addEventListener('fetch', (e) => {
|
|
17
|
+
const req = e.request;
|
|
18
|
+
if (req.method !== 'GET') return;
|
|
19
|
+
|
|
20
|
+
${n==="cache-first"?`
|
|
21
|
+
e.respondWith(
|
|
22
|
+
caches.match(req).then((cached) => cached || fetch(req).then((res) => {
|
|
23
|
+
const clone = res.clone();
|
|
24
|
+
caches.open(CACHE).then((c) => c.put(req, clone));
|
|
25
|
+
return res;
|
|
26
|
+
})).catch(() => OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
|
|
27
|
+
);`:n==="stale-while-revalidate"?`
|
|
28
|
+
e.respondWith(
|
|
29
|
+
caches.match(req).then((cached) => {
|
|
30
|
+
const fetched = fetch(req).then((res) => {
|
|
31
|
+
caches.open(CACHE).then((c) => c.put(req, res.clone()));
|
|
32
|
+
return res;
|
|
33
|
+
});
|
|
34
|
+
return cached || fetched;
|
|
35
|
+
}).catch(() => OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
|
|
36
|
+
);`:`
|
|
37
|
+
e.respondWith(
|
|
38
|
+
fetch(req).then((res) => {
|
|
39
|
+
const clone = res.clone();
|
|
40
|
+
caches.open(CACHE).then((c) => c.put(req, clone));
|
|
41
|
+
return res;
|
|
42
|
+
}).catch(() => caches.match(req).then((cached) =>
|
|
43
|
+
cached || (OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
|
|
44
|
+
))
|
|
45
|
+
);`}
|
|
46
|
+
});
|
|
47
|
+
`.trim()}function ke(t){let n={name:t.name,short_name:t.shortName??t.name,start_url:"/",display:t.display??"standalone",background_color:t.backgroundColor??"#ffffff",theme_color:t.themeColor??"#000000"};return t.icon&&(n.icons=[{src:t.icon,sizes:"192x192",type:"image/png"},{src:t.icon,sizes:"512x512",type:"image/png"}]),n}var Se={register(t){let n=ke(t),e=new Blob([JSON.stringify(n)],{type:"application/json"}),o=document.createElement("link");o.rel="manifest",o.href=URL.createObjectURL(e),document.head.appendChild(o);let r=document.querySelector('meta[name="theme-color"]');r||(r=document.createElement("meta"),r.name="theme-color",document.head.appendChild(r)),r.content=t.themeColor??"#000000","serviceWorker"in navigator&&(t.swUrl?navigator.serviceWorker.register(t.swUrl).catch(s=>{console.warn("[tina4] Service worker registration failed:",s)}):navigator.serviceWorker.register("/sw.js").catch(()=>{console.info("[tina4] No service worker at /sw.js. Use pwa.generateServiceWorker() to create one, or pass swUrl in config.")}))},generateServiceWorker(t){return Fe(t)},generateManifest(t){return ke(t)}};var We={reconnect:!0,reconnectDelay:1e3,reconnectMaxDelay:3e4,reconnectAttempts:1/0,protocols:[]};function $e(t,n={}){let e={...We,...n},o=v("connecting"),r=v(!1),s=v(null),i=v(null),a=v(0),c={message:[],open:[],close:[],error:[]},l=null,d=!1,u=e.reconnectDelay,g=null,w=0;function C(p){if(typeof p!="string")return p;try{return JSON.parse(p)}catch{return p}}function E(){o.value=w>0?"reconnecting":"connecting";try{l=new WebSocket(t,e.protocols)}catch{o.value="closed",r.value=!1;return}l.onopen=()=>{o.value="open",r.value=!0,i.value=null,w=0,u=e.reconnectDelay,a.value=0;for(let p of c.open)p()},l.onmessage=p=>{let h=C(p.data);s.value=h;for(let k of c.message)k(h)},l.onclose=p=>{o.value="closed",r.value=!1;for(let h of c.close)h(p.code,p.reason);!d&&e.reconnect&&w<e.reconnectAttempts&&x()},l.onerror=p=>{i.value=p;for(let h of c.error)h(p)}}function x(){w++,a.value=w,o.value="reconnecting",g=setTimeout(()=>{g=null,E()},u),u=Math.min(u*2,e.reconnectMaxDelay)}let _={status:o,connected:r,lastMessage:s,error:i,reconnectCount:a,send(p){if(!l||l.readyState!==WebSocket.OPEN)throw new Error("[tina4] WebSocket is not connected");let h=typeof p=="string"?p:JSON.stringify(p);l.send(h)},on(p,h){return c[p].push(h),()=>{let k=c[p],A=k.indexOf(h);A>=0&&k.splice(A,1)}},pipe(p,h){let k=A=>{p.value=h(A,p.value)};return _.on("message",k)},close(p,h){d=!0,g&&(clearTimeout(g),g=null),l&&l.close(p??1e3,h??""),o.value="closed",r.value=!1}};return E(),_}var Ee={connect:$e};var Ge={mode:"eventsource",method:"GET",headers:{},body:void 0,reconnect:!0,reconnectDelay:1e3,reconnectMaxDelay:3e4,reconnectAttempts:1/0,events:[],json:!0};function Je(t,n={}){let e={...Ge,...n},o=v("connecting"),r=v(!1),s=v(null),i=v(null),a=v(null),c=v(0),l={message:[],open:[],close:[],error:[]},d=null,u=null,g=!1,w=e.reconnectDelay,C=null,E=0;function x(f){if(!e.json||typeof f!="string")return f;try{return JSON.parse(f)}catch{return f}}function _(f,m){s.value=f,i.value=m;for(let T of l.message)T(f,m??void 0)}function p(){o.value="open",r.value=!0,a.value=null,E=0,w=e.reconnectDelay,c.value=0;for(let f of l.open)f()}function h(){o.value="closed",r.value=!1;for(let f of l.close)f();!g&&e.reconnect&&E<e.reconnectAttempts&&_e()}function k(f){a.value=f;for(let m of l.error)m(f)}function A(){o.value=E>0?"reconnecting":"connecting";try{d=new EventSource(t)}catch{o.value="closed",r.value=!1;return}d.onopen=()=>p(),d.onmessage=f=>{_(x(f.data),null)};for(let f of e.events)d.addEventListener(f,m=>{_(x(m.data),f)});d.onerror=f=>{k(f),d&&d.readyState===2&&(d=null,h())}}function Ce(){o.value=E>0?"reconnecting":"connecting",u=new AbortController;let f={method:e.method,headers:e.headers,signal:u.signal};e.body!==void 0&&(f.body=typeof e.body=="string"?e.body:JSON.stringify(e.body)),fetch(t,f).then(async m=>{if(!m.ok){k(new Error(`[tina4] SSE fetch ${m.status}`)),h();return}p();let T=m.body.getReader(),M=new TextDecoder,P="";for(;;){let{done:Re,value:xe}=await T.read();if(Re)break;P+=M.decode(xe,{stream:!0});let re=P.split(`
|
|
48
|
+
`);P=re.pop();for(let Ae of re){let se=Ae.trim();se&&_(x(se),null)}}let oe=P.trim();oe&&_(x(oe),null),u=null,h()}).catch(m=>{m.name!=="AbortError"&&(u=null,k(m),h())})}function _e(){E++,c.value=E,o.value="reconnecting",C=setTimeout(()=>{C=null,te()},w),w=Math.min(w*2,e.reconnectMaxDelay)}function te(){e.mode==="fetch"?Ce():A()}let ne={status:o,connected:r,lastMessage:s,lastEvent:i,error:a,reconnectCount:c,on(f,m){return l[f].push(m),()=>{let T=l[f],M=T.indexOf(m);M>=0&&T.splice(M,1)}},pipe(f,m){let T=M=>{f.value=m(M,f.value)};return ne.on("message",T)},close(){g=!0,C&&(clearTimeout(C),C=null),d&&(d.close(),d=null),u&&(u.abort(),u=null),o.value="closed",r.value=!1}};return te(),ne}var Te={connect:Je};return De(Ve);})();
|
|
@@ -438,9 +438,38 @@ class WebSocketServer:
|
|
|
438
438
|
self._server: asyncio.AbstractServer | None = None
|
|
439
439
|
|
|
440
440
|
def route(self, path: str):
|
|
441
|
-
"""Decorator to register a WebSocket handler for a path.
|
|
441
|
+
"""Decorator to register a WebSocket handler for a path.
|
|
442
|
+
|
|
443
|
+
Registers both on this server instance (standalone mode) and on the
|
|
444
|
+
main Router (integrated mode) so routes work either way.
|
|
445
|
+
|
|
446
|
+
The handler uses WebSocketServer style: ``async def handler(conn)``
|
|
447
|
+
with ``@conn.on_message`` / ``@conn.on_close`` decorators.
|
|
448
|
+
This is automatically adapted to the Router's ``(conn, event, data)``
|
|
449
|
+
style for integrated mode.
|
|
450
|
+
"""
|
|
442
451
|
def decorator(func):
|
|
443
452
|
self._handlers[path] = {"handler": func}
|
|
453
|
+
|
|
454
|
+
# Adapt to Router's (conn, event, data) style
|
|
455
|
+
async def _router_adapter(conn, event, data):
|
|
456
|
+
if event == "open":
|
|
457
|
+
result = func(conn)
|
|
458
|
+
if asyncio.iscoroutine(result):
|
|
459
|
+
await result
|
|
460
|
+
elif event == "message":
|
|
461
|
+
if conn._on_message:
|
|
462
|
+
result = conn._on_message(data)
|
|
463
|
+
if asyncio.iscoroutine(result):
|
|
464
|
+
await result
|
|
465
|
+
elif event == "close":
|
|
466
|
+
if conn._on_close:
|
|
467
|
+
result = conn._on_close()
|
|
468
|
+
if asyncio.iscoroutine(result):
|
|
469
|
+
await result
|
|
470
|
+
|
|
471
|
+
from tina4_python.core.router import Router
|
|
472
|
+
Router.websocket(path, _router_adapter)
|
|
444
473
|
return func
|
|
445
474
|
return decorator
|
|
446
475
|
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";var Tina4=(()=>{var H=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var ge=Object.getOwnPropertyNames;var he=Object.prototype.hasOwnProperty;var me=(e,n)=>{for(var t in n)H(e,t,{get:n[t],enumerable:!0})},ye=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of ge(n))!he.call(e,r)&&r!==t&&H(e,r,{get:()=>n[r],enumerable:!(o=pe(n,r))||o.enumerable});return e};var ve=e=>ye(H({},"__esModule",{value:!0}),e);var Ae={};me(Ae,{Tina4Element:()=>I,api:()=>ie,batch:()=>q,computed:()=>z,effect:()=>m,html:()=>X,isSignal:()=>w,navigate:()=>L,pwa:()=>le,route:()=>oe,router:()=>re,signal:()=>h,ws:()=>ue});var C=null,_=null,M=null;function b(e){M=e}function J(){return M}var B=null,V=null;var N=0,P=new Set;function h(e,n){let t=e,o=new Set,r={_t4:!0,get value(){if(C&&(o.add(C),_)){let a=C;_.push(()=>o.delete(a))}return t},set value(a){if(Object.is(a,t))return;let i=t;if(t=a,r._debugInfo&&r._debugInfo.updateCount++,V&&V(r,i,a),N>0)for(let s of o)P.add(s);else{let s;for(let c of[...o])try{c()}catch(l){s===void 0&&(s=l)}if(s!==void 0)throw s}},_subscribe(a){return o.add(a),()=>{o.delete(a)}},peek(){return t}};return B&&(r._debugInfo={label:n,createdAt:Date.now(),updateCount:0,subs:o},B(r,n)),r}function z(e){let n=h(void 0);return m(()=>{n.value=e()}),{_t4:!0,get value(){return n.value},set value(t){throw new Error("[tina4] computed signals are read-only")},_subscribe(t){return n._subscribe(t)},peek(){return n.peek()}}}function m(e){let n=!1,t=[],o=()=>{if(n)return;for(let s of t)s();t=[];let a=C,i=_;C=o,_=t;try{e()}finally{C=a,_=i}};o();let r=()=>{n=!0;for(let a of t)a();t=[]};return M&&M.push(r),r}function q(e){N++;try{e()}finally{if(N--,N===0){let n=[...P];P.clear();let t;for(let o of n)try{o()}catch(r){t===void 0&&(t=r)}if(t!==void 0)throw t}}}function w(e){return e!==null&&typeof e=="object"&&e._t4===!0}var Q=new WeakMap,D="t4:";function X(e,...n){let t=Q.get(e);if(!t){t=document.createElement("template");let i="";for(let s=0;s<e.length;s++)i+=e[s],s<n.length&&(Te(i)?i+=`__t4_${s}__`:i+=`<!--${D}${s}-->`);t.innerHTML=i,Q.set(e,t)}let o=t.content.cloneNode(!0),r=be(o);for(let{marker:i,index:s}of r)ke(i,n[s]);let a=we(o);for(let i of a)Ce(i,n);return o}function be(e){let n=[];return W(e,t=>{if(t.nodeType===8){let o=t.data;if(o&&o.startsWith(D)){let r=parseInt(o.slice(D.length),10);n.push({marker:t,index:r})}}}),n}function we(e){let n=[];return W(e,t=>{t.nodeType===1&&n.push(t)}),n}function W(e,n){let t=e.childNodes;for(let o=0;o<t.length;o++){let r=t[o];n(r),W(r,n)}}function ke(e,n){let t=e.parentNode;if(t)if(w(n)){let o=document.createTextNode("");t.replaceChild(o,e),m(()=>{o.data=String(n.value??"")})}else if(typeof n=="function"){let o=document.createComment("");t.replaceChild(o,e);let r=[],a=[];m(()=>{for(let d of a)d();a=[];let i=[],s=J();b(i);let c=n();b(s),a=i;for(let d of r)d.parentNode?.removeChild(d);r=[];let l=F(c),f=o.parentNode;if(f)for(let d of l)f.insertBefore(d,o),r.push(d)})}else if(Y(n))t.replaceChild(n,e);else if(n instanceof Node)t.replaceChild(n,e);else if(Array.isArray(n)){let o=document.createDocumentFragment();for(let r of n){let a=F(r);for(let i of a)o.appendChild(i)}t.replaceChild(o,e)}else{let o=document.createTextNode(String(n??""));t.replaceChild(o,e)}}function Ce(e,n){let t=[];for(let o of Array.from(e.attributes)){let r=o.name,a=o.value;if(r.startsWith("@")){let s=r.slice(1),c=a.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];typeof l=="function"&&e.addEventListener(s,f=>q(()=>l(f)))}t.push(r);continue}if(r.startsWith("?")){let s=r.slice(1),c=a.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];if(w(l)){let f=l;m(()=>{f.value?e.setAttribute(s,""):e.removeAttribute(s)})}else typeof l=="function"?m(()=>{l()?e.setAttribute(s,""):e.removeAttribute(s)}):l&&e.setAttribute(s,"")}t.push(r);continue}if(r.startsWith(".")){let s=r.slice(1),c=a.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];w(l)?m(()=>{e[s]=l.value}):e[s]=l}t.push(r);continue}let i=a.match(/__t4_(\d+)__/);if(i){let s=n[parseInt(i[1],10)];if(w(s)){let c=s;m(()=>{e.setAttribute(r,String(c.value??""))})}else typeof s=="function"?m(()=>{e.setAttribute(r,String(s()??""))}):e.setAttribute(r,String(s??""))}}for(let o of t)e.removeAttribute(o)}function F(e){if(e==null||e===!1)return[];if(Y(e))return Array.from(e.childNodes);if(e instanceof Node)return[e];if(Array.isArray(e)){let n=[];for(let t of e)n.push(...F(t));return n}return[document.createTextNode(String(e))]}function Y(e){return e!=null&&typeof e=="object"&&e.nodeType===11}function Te(e){let n=!1,t=!1,o=!1;for(let r=0;r<e.length;r++){let a=e[r];a==="<"&&!n&&!t&&(o=!0),a===">"&&!n&&!t&&(o=!1),o&&(a==='"'&&!n&&(t=!t),a==="'"&&!t&&(n=!n))}return o}var Z=null,ee=null;var I=class extends HTMLElement{constructor(){super();this._props={};this._rendered=!1;let t=this.constructor;this._root=t.shadow?this.attachShadow({mode:"open"}):this;for(let[o,r]of Object.entries(t.props))this._props[o]=h(this._coerce(this.getAttribute(o),r))}static{this.props={}}static{this.styles=""}static{this.shadow=!0}static get observedAttributes(){return Object.keys(this.props)}connectedCallback(){if(this._rendered)return;this._rendered=!0;let t=this.constructor;if(t.styles&&t.shadow&&this._root instanceof ShadowRoot){let r=document.createElement("style");r.textContent=t.styles,this._root.appendChild(r)}let o=this.render();o&&this._root.appendChild(o),this.onMount(),Z&&Z(this)}disconnectedCallback(){this.onUnmount(),ee&&ee(this)}attributeChangedCallback(t,o,r){let i=this.constructor.props[t];i&&this._props[t]&&(this._props[t].value=this._coerce(r,i))}prop(t){if(!this._props[t])throw new Error(`[tina4] Prop '${t}' not declared in static props of <${this.tagName.toLowerCase()}>`);return this._props[t]}emit(t,o){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,...o}))}onMount(){}onUnmount(){}_coerce(t,o){return o===Boolean?t!==null:o===Number?t!==null?Number(t):0:t??""}};var U=[],T=null,S="history",_e=!1,E=[],O=[],te=0;function oe(e,n){let t=[],o;e==="*"?o=".*":o=e.replace(/\{(\w+)\}/g,(a,i)=>(t.push(i),"([^/]+)"));let r=new RegExp(`^${o}$`);typeof n=="function"?U.push({pattern:e,regex:r,paramNames:t,handler:n}):U.push({pattern:e,regex:r,paramNames:t,handler:n.handler,guard:n.guard})}function L(e,n){if(S==="hash")if(n?.replace){let t=new URL(location.href);t.hash="#"+e,history.replaceState(null,"",t.toString()),R()}else location.hash="#"+e;else n?.replace?history.replaceState(null,"",e):history.pushState(null,"",e),R()}function R(){if(!T)return;let e=performance.now(),n=++te,t=S==="hash"?location.hash.slice(1)||"/":location.pathname;for(let o of U){let r=t.match(o.regex);if(!r)continue;let a={};if(o.paramNames.forEach((c,l)=>{a[c]=decodeURIComponent(r[l+1])}),o.guard){let c=o.guard();if(c===!1)return;if(typeof c=="string"){L(c,{replace:!0});return}}for(let c of O)c();O=[],T.innerHTML="";let i=[];b(i);let s=o.handler(a);if(s instanceof Promise)s.then(c=>{if(b(null),n!==te){for(let f of i)f();return}ne(T,c),O=i;let l=performance.now()-e;for(let f of E)f({path:t,params:a,pattern:o.pattern,durationMs:l})});else{b(null),ne(T,s),O=i;let c=performance.now()-e;for(let l of E)l({path:t,params:a,pattern:o.pattern,durationMs:c})}return}}function ne(e,n){n instanceof DocumentFragment||n instanceof Node?e.replaceChildren(n):typeof n=="string"?e.innerHTML=n:n!=null&&e.replaceChildren(document.createTextNode(String(n)))}var re={start(e){if(T=document.querySelector(e.target),!T)throw new Error(`[tina4] Router target '${e.target}' not found in DOM`);S=e.mode??"history",_e=!0,window.addEventListener("popstate",R),S==="hash"&&window.addEventListener("hashchange",R),document.addEventListener("click",n=>{if(n.metaKey||n.ctrlKey||n.shiftKey||n.altKey)return;let t=n.target.closest("a[href]");if(!t||t.origin!==location.origin||t.hasAttribute("target")||t.hasAttribute("download")||t.getAttribute("rel")?.includes("external"))return;n.preventDefault();let o=S==="hash"?t.getAttribute("href"):t.pathname;L(o)}),R()},on(e,n){return E.push(n),()=>{let t=E.indexOf(n);t>=0&&E.splice(t,1)}}};var y={baseUrl:"",auth:!1,tokenKey:"tina4_token",headers:{}},j=[],K=[],Se=0;function se(){try{return localStorage.getItem(y.tokenKey)}catch{return null}}function Ee(e){try{localStorage.setItem(y.tokenKey,e)}catch{}}async function x(e,n,t,o){let r={method:e,headers:{"Content-Type":"application/json",...y.headers}};if(y.auth){let d=se();d&&(r.headers.Authorization=`Bearer ${d}`)}if(t!==void 0&&e!=="GET"){let d=typeof t=="object"&&t!==null?{...t}:t;if(y.auth&&typeof d=="object"&&d!==null){let p=se();p&&(d.formToken=p)}r.body=JSON.stringify(d)}if(o?.headers&&Object.assign(r.headers,o.headers),o?.params){let d=Object.entries(o.params).map(([p,v])=>`${encodeURIComponent(p)}=${encodeURIComponent(String(v))}`).join("&");n+=(n.includes("?")?"&":"?")+d}let a=y.baseUrl+n;r._url=a,r._requestId=++Se;for(let d of j){let p=d(r);p&&(r=p)}let i=await fetch(a,r),s=i.headers.get("FreshToken");s&&Ee(s);let c=i.headers.get("Content-Type")??"",l;c.includes("json")?l=await i.json():l=await i.text();let f={status:i.status,data:l,ok:i.ok,headers:i.headers,_requestId:r._requestId};for(let d of K){let p=d(f);p&&(f=p)}if(!i.ok)throw f;return f.data}var ie={configure(e){Object.assign(y,e)},get(e,n){return x("GET",e,void 0,n)},post(e,n,t){return x("POST",e,n,t)},put(e,n,t){return x("PUT",e,n,t)},patch(e,n,t){return x("PATCH",e,n,t)},delete(e,n){return x("DELETE",e,void 0,n)},intercept(e,n){e==="request"?j.push(n):K.push(n)},_reset(){y.baseUrl="",y.auth=!1,y.tokenKey="tina4_token",y.headers={},j.length=0,K.length=0}};function ae(e){let n=e.cacheStrategy??"network-first",t=JSON.stringify(e.precache??[]),o=e.offlineRoute?`'${e.offlineRoute}'`:"null";return`
|
|
2
|
-
const CACHE = 'tina4-v1';
|
|
3
|
-
const PRECACHE = ${t};
|
|
4
|
-
const OFFLINE = ${o};
|
|
5
|
-
|
|
6
|
-
self.addEventListener('install', (e) => {
|
|
7
|
-
e.waitUntil(
|
|
8
|
-
caches.open(CACHE).then((c) => c.addAll(PRECACHE)).then(() => self.skipWaiting())
|
|
9
|
-
);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
self.addEventListener('activate', (e) => {
|
|
13
|
-
e.waitUntil(self.clients.claim());
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
self.addEventListener('fetch', (e) => {
|
|
17
|
-
const req = e.request;
|
|
18
|
-
if (req.method !== 'GET') return;
|
|
19
|
-
|
|
20
|
-
${n==="cache-first"?`
|
|
21
|
-
e.respondWith(
|
|
22
|
-
caches.match(req).then((cached) => cached || fetch(req).then((res) => {
|
|
23
|
-
const clone = res.clone();
|
|
24
|
-
caches.open(CACHE).then((c) => c.put(req, clone));
|
|
25
|
-
return res;
|
|
26
|
-
})).catch(() => OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
|
|
27
|
-
);`:n==="stale-while-revalidate"?`
|
|
28
|
-
e.respondWith(
|
|
29
|
-
caches.match(req).then((cached) => {
|
|
30
|
-
const fetched = fetch(req).then((res) => {
|
|
31
|
-
caches.open(CACHE).then((c) => c.put(req, res.clone()));
|
|
32
|
-
return res;
|
|
33
|
-
});
|
|
34
|
-
return cached || fetched;
|
|
35
|
-
}).catch(() => OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
|
|
36
|
-
);`:`
|
|
37
|
-
e.respondWith(
|
|
38
|
-
fetch(req).then((res) => {
|
|
39
|
-
const clone = res.clone();
|
|
40
|
-
caches.open(CACHE).then((c) => c.put(req, clone));
|
|
41
|
-
return res;
|
|
42
|
-
}).catch(() => caches.match(req).then((cached) =>
|
|
43
|
-
cached || (OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
|
|
44
|
-
))
|
|
45
|
-
);`}
|
|
46
|
-
});
|
|
47
|
-
`.trim()}function ce(e){let n={name:e.name,short_name:e.shortName??e.name,start_url:"/",display:e.display??"standalone",background_color:e.backgroundColor??"#ffffff",theme_color:e.themeColor??"#000000"};return e.icon&&(n.icons=[{src:e.icon,sizes:"192x192",type:"image/png"},{src:e.icon,sizes:"512x512",type:"image/png"}]),n}var le={register(e){let n=ce(e),t=new Blob([JSON.stringify(n)],{type:"application/json"}),o=document.createElement("link");o.rel="manifest",o.href=URL.createObjectURL(t),document.head.appendChild(o);let r=document.querySelector('meta[name="theme-color"]');if(r||(r=document.createElement("meta"),r.name="theme-color",document.head.appendChild(r)),r.content=e.themeColor??"#000000","serviceWorker"in navigator){let a=ae(e),i=new Blob([a],{type:"text/javascript"}),s=URL.createObjectURL(i);navigator.serviceWorker.register(s).catch(c=>{console.warn("[tina4] Service worker registration failed:",c)})}},generateServiceWorker(e){return ae(e)},generateManifest(e){return ce(e)}};var Re={reconnect:!0,reconnectDelay:1e3,reconnectMaxDelay:3e4,reconnectAttempts:1/0,protocols:[]};function xe(e,n={}){let t={...Re,...n},o=h("connecting"),r=h(!1),a=h(null),i=h(null),s=h(0),c={message:[],open:[],close:[],error:[]},l=null,f=!1,d=t.reconnectDelay,p=null,v=0;function de(u){if(typeof u!="string")return u;try{return JSON.parse(u)}catch{return u}}function $(){o.value=v>0?"reconnecting":"connecting";try{l=new WebSocket(e,t.protocols)}catch{o.value="closed",r.value=!1;return}l.onopen=()=>{o.value="open",r.value=!0,i.value=null,v=0,d=t.reconnectDelay,s.value=0;for(let u of c.open)u()},l.onmessage=u=>{let g=de(u.data);a.value=g;for(let k of c.message)k(g)},l.onclose=u=>{o.value="closed",r.value=!1;for(let g of c.close)g(u.code,u.reason);!f&&t.reconnect&&v<t.reconnectAttempts&&fe()},l.onerror=u=>{i.value=u;for(let g of c.error)g(u)}}function fe(){v++,s.value=v,o.value="reconnecting",p=setTimeout(()=>{p=null,$()},d),d=Math.min(d*2,t.reconnectMaxDelay)}let G={status:o,connected:r,lastMessage:a,error:i,reconnectCount:s,send(u){if(!l||l.readyState!==WebSocket.OPEN)throw new Error("[tina4] WebSocket is not connected");let g=typeof u=="string"?u:JSON.stringify(u);l.send(g)},on(u,g){return c[u].push(g),()=>{let k=c[u],A=k.indexOf(g);A>=0&&k.splice(A,1)}},pipe(u,g){let k=A=>{u.value=g(A,u.value)};return G.on("message",k)},close(u,g){f=!0,p&&(clearTimeout(p),p=null),l&&l.close(u??1e3,g??""),o.value="closed",r.value=!1}};return $(),G}var ue={connect:xe};return ve(Ae);})();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|