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.
Files changed (152) hide show
  1. {tina4_python-3.10.93 → tina4_python-3.10.95}/PKG-INFO +1 -1
  2. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/CLAUDE.md +22 -10
  3. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/server.py +134 -27
  5. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dev_reload.py +1 -1
  6. tina4_python-3.10.95/tina4_python/public/js/tina4js.min.js +48 -0
  7. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/websocket/__init__.py +30 -1
  8. tina4_python-3.10.93/tina4_python/public/js/tina4js.min.js +0 -47
  9. {tina4_python-3.10.93 → tina4_python-3.10.95}/.gitignore +0 -0
  10. {tina4_python-3.10.93 → tina4_python-3.10.95}/README.md +0 -0
  11. {tina4_python-3.10.93 → tina4_python-3.10.95}/pyproject.toml +0 -0
  12. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/HtmlElement.py +0 -0
  13. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/Testing.py +0 -0
  14. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/core/router.py +0 -0
  29. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/crud/__init__.py +0 -0
  30. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/__init__.py +0 -0
  31. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/adapter.py +0 -0
  32. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/connection.py +0 -0
  33. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/firebird.py +0 -0
  34. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/mongodb.py +0 -0
  35. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/mssql.py +0 -0
  36. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/mysql.py +0 -0
  37. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/odbc.py +0 -0
  38. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/postgres.py +0 -0
  39. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/database/sqlite.py +0 -0
  40. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/debug/__init__.py +0 -0
  41. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/debug/error_overlay.py +0 -0
  42. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dev_admin/__init__.py +0 -0
  43. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dev_admin/metrics.py +0 -0
  44. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/frond/engine.py +0 -0
  48. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/css/tina4.css +0 -0
  76. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/css/tina4.min.css +0 -0
  77. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/favicon.ico +0 -0
  78. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/images/logo.svg +0 -0
  79. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  80. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/frond.min.js +0 -0
  81. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  82. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  83. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/js/tina4.min.js +0 -0
  84. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/swagger/index.html +0 -0
  85. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  86. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/query_builder/__init__.py +0 -0
  87. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/__init__.py +0 -0
  88. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/job.py +0 -0
  89. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/kafka_backend.py +0 -0
  90. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/lite_backend.py +0 -0
  91. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/mongo_backend.py +0 -0
  92. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue/rabbitmq_backend.py +0 -0
  93. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/__init__.py +0 -0
  94. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/kafka_backend.py +0 -0
  95. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/mongo_backend.py +0 -0
  96. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/__init__.py +0 -0
  98. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  99. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_badges.scss +0 -0
  100. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  101. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_cards.scss +0 -0
  102. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_forms.scss +0 -0
  103. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_grid.scss +0 -0
  104. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_modals.scss +0 -0
  105. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_nav.scss +0 -0
  106. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_reset.scss +0 -0
  107. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_tables.scss +0 -0
  108. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_typography.scss +0 -0
  109. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  110. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/_variables.scss +0 -0
  111. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/base.scss +0 -0
  112. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/colors.scss +0 -0
  113. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/scss/tina4css/tina4.scss +0 -0
  114. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/seeder/__init__.py +0 -0
  115. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/service/__init__.py +0 -0
  116. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session/__init__.py +0 -0
  117. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/__init__.py +0 -0
  118. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  119. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/redis_handler.py +0 -0
  120. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/session_handlers/valkey_handler.py +0 -0
  121. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/swagger/__init__.py +0 -0
  122. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/components/crud.twig +0 -0
  123. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  124. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  125. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/python/Dockerfile +0 -0
  126. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  127. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/302.twig +0 -0
  128. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/401.twig +0 -0
  129. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/403.twig +0 -0
  130. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/404.twig +0 -0
  131. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/500.twig +0 -0
  132. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/502.twig +0 -0
  133. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/503.twig +0 -0
  134. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/errors/base.twig +0 -0
  135. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/frontend/README.md +0 -0
  136. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/templates/readme.md +0 -0
  137. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/test_client/__init__.py +0 -0
  138. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  139. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  140. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  141. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  142. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/validator/__init__.py +0 -0
  151. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/websocket/backplane.py +0 -0
  152. {tina4_python-3.10.93 → tina4_python-3.10.95}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.10.93
3
+ Version: 3.10.95
4
4
  Summary: Tina4 for Python — 54 built-in features, zero dependencies
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -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
- uv run tina4python start # Start dev server on port 7145
380
- uv run tina4python serve --production # Auto-install and use uvicorn
381
- uv run tina4python init . # Scaffold project structure
382
- uv run tina4python migrate # Run pending SQL migrations
383
- uv run tina4python migrate:create "desc" # Create a migration file
384
- uv run tina4python generate model <name> # Generate ORM model scaffold
385
- uv run tina4python generate route <name> # Generate route scaffold
386
- uv run tina4python generate migration <d> # Generate migration file
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,066 tests** passing across all modules
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()`)
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.10.93"
11
+ __version__ = "3.10.95"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -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
- await handler(conn, "message", data)
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
- await handler(conn, "close", None)
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
- # Wire up message/close callbacks and run the frame loop
621
- async def on_message(message):
622
- try:
623
- await handler(ws, "message", message)
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
- ws._on_message = on_message
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
- original_on_close = ws._on_close
693
+ if not decorator_on_close:
694
+ original_on_close = ws._on_close
630
695
 
631
- async def on_close():
632
- try:
633
- await handler(ws, "close", None)
634
- except Exception as e:
635
- Log.error(f"WebSocket close handler error: {e}")
636
- _ws_manager.remove(ws)
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
- ws._on_close = on_close
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
- await handler(ws, "close", None)
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 Rust CLI watcher.
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