├── instant
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── runws.py
│ │ └── installws.py
├── migrations
│ ├── __init__.py
│ ├── 0002_alter_channel_level.py
│ └── 0001_initial.py
├── tests
│ ├── __init__.py
│ ├── urls.py
│ ├── test_utils.py
│ ├── test_producers.py
│ ├── base.py
│ ├── test_token.py
│ ├── test_views.py
│ ├── test_model.py
│ ├── test_conf.py
│ └── runtests.py
├── __init__.py
├── admin.py
├── apps.py
├── conf.py
├── urls.py
├── token.py
├── producers.py
├── init.py
├── models.py
├── views.py
└── static
│ └── instant
│ └── index.min.js
├── requirements.txt
├── MANIFEST.in
├── .travis.yml
├── setup.cfg
├── tox.ini
├── pyrightconfig.json
├── .github
└── workflows
│ └── django.yml
├── .gitignore
├── LICENSE
├── setup.py
└── README.md
/instant/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/instant/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | cent
2 | PyJWT
--------------------------------------------------------------------------------
/instant/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/instant/tests/__init__.py:
--------------------------------------------------------------------------------
1 | class InstantTest:
2 | pass
3 |
--------------------------------------------------------------------------------
/instant/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "4.2.0"
2 | default_app_config = "instant.apps.InstantConfig"
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include instant/migrations *
2 | recursive-include instant/management *
3 | recursive-include instant/static *
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - 3.6
5 | - 3.7
6 |
7 | env:
8 | global:
9 | - PYTHONPATH="/home/travis/build/synw/django-instant"
10 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 | [flake8]
5 | max-line-length = 88
6 | exclude =
7 | .git,
8 | .venv,
9 | build,
10 | __pycache__
11 | */migrations/*
--------------------------------------------------------------------------------
/instant/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from django.contrib import admin
3 |
4 | urlpatterns = [
5 | path("admin/", admin.site.urls),
6 | path("instant/", include("instant.urls")),
7 | ]
8 |
--------------------------------------------------------------------------------
/instant/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from instant.models import Channel
4 |
5 |
6 | @admin.register(Channel) # type: ignore
7 | class ChannelAdmin(admin.ModelAdmin):
8 | list_display = ["name", "level", "is_active"]
9 |
--------------------------------------------------------------------------------
/instant/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.apps import AppConfig
3 |
4 |
5 | class InstantConfig(AppConfig):
6 | name = "instant"
7 | default_auto_field = "django.db.models.BigAutoField"
8 |
9 | def ready(self):
10 | pass
11 |
--------------------------------------------------------------------------------
/instant/conf.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | CENTRIFUGO_API_KEY = getattr(settings, "CENTRIFUGO_API_KEY", None)
5 | CENTRIFUGO_HMAC_KEY = getattr(settings, "CENTRIFUGO_HMAC_KEY", None)
6 | CENTRIFUGO_HOST = getattr(settings, "CENTRIFUGO_HOST", "http://localhost")
7 | CENTRIFUGO_PORT = getattr(settings, "CENTRIFUGO_PORT", 8427)
8 |
9 | SITE_SLUG = getattr(settings, "SITE_SLUG", "site")
10 | SITE_NAME = getattr(settings, "SITE_NAME", "Site")
11 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [base]
2 | deps =
3 | cent
4 | PyJWT
5 |
6 | [testenv:django3]
7 | deps =
8 | django>=3.0
9 | {[base]deps}
10 |
11 | [testenv:coverage]
12 | setenv =
13 | PYTHONPATH = {toxinidir}
14 | commands =
15 | coverage run instant/tests/runtests.py
16 | deps =
17 | coverage
18 | {[testenv:django3]deps}
19 |
20 | [pytest]
21 | DJANGO_SETTINGS_MODULE = instant.tests.runtests
22 | python_files = tests.py test_*.py *_tests.py
23 |
24 |
25 |
--------------------------------------------------------------------------------
/instant/urls.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.urls import path
3 | from .views import (
4 | login_and_get_tokens,
5 | logout,
6 | get_connection_token,
7 | channels_subscription,
8 | )
9 |
10 | urlpatterns = [
11 | path("login/", login_and_get_tokens, name="instant-login"),
12 | path("logout/", logout, name="instant-logout"),
13 | path("get_token/", get_connection_token, name="instant-get-token"),
14 | path("subscribe/", channels_subscription, name="instant-subscribe"),
15 | ]
16 |
--------------------------------------------------------------------------------
/instant/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from .base import InstantBaseTest
2 |
3 | from instant.init import ensure_channel_is_private
4 |
5 |
6 | class InstantTestUtils(InstantBaseTest):
7 | def test_ensure_channel_is_private(self):
8 | name = ensure_channel_is_private("$chan")
9 | self.assertEqual(name, "$chan")
10 | name = ensure_channel_is_private("chan")
11 | self.assertEqual(name, "$chan")
12 | name = ensure_channel_is_private("ns:$chan")
13 | self.assertEqual(name, "ns:$chan")
14 | name = ensure_channel_is_private("ns:chan")
15 | self.assertEqual(name, "ns:$chan")
16 |
--------------------------------------------------------------------------------
/instant/migrations/0002_alter_channel_level.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0.1 on 2022-02-01 09:08
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('instant', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='channel',
15 | name='level',
16 | field=models.CharField(choices=[('public', 'Public'), ('users', 'Users'), ('groups', 'Groups'), ('staff', 'Staff'), ('superuser', 'Superuser')], default='superuser', max_length=20, verbose_name='Authorized for'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/pyrightconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "useLibraryCodeForTypes": true,
3 | "reportMissingImports": false,
4 | "reportMissingTypeStubs": false,
5 | "strictSetInference": true,
6 | "strictDictionaryInference": true,
7 | "strictListInference": true,
8 | "reportUntypedFunctionDecorator": true,
9 | "reportUntypedClassDecorator": true,
10 | "reportIncompatibleMethodOverride": "error",
11 | "reportIncompatibleVariableOverride": "error",
12 | "reportUninitializedInstanceVariable": "error",
13 | "reportUnknownParameterType": false,
14 | "reportUnknownVariableType": false,
15 | "reportUnnecessaryCast": "warning",
16 | "reportUnnecessaryComparison": "warning",
17 | }
--------------------------------------------------------------------------------
/instant/tests/test_producers.py:
--------------------------------------------------------------------------------
1 | from cent import CentException
2 |
3 | from .base import InstantBaseTest
4 |
5 | from instant.producers import publish
6 |
7 |
8 | class InstantTestProducers(InstantBaseTest):
9 | def test_publish(self):
10 | with self.settings(CENTRIFUGO_API_KEY=None) and self.assertRaises(ValueError):
11 | publish("chan")
12 | with self.assertRaises(CentException):
13 | publish("chan", "message")
14 | with self.assertRaises(CentException):
15 | publish(
16 | "chan",
17 | data={"foo": 1},
18 | event_class="important",
19 | bucket="testing",
20 | )
21 |
--------------------------------------------------------------------------------
/.github/workflows/django.yml:
--------------------------------------------------------------------------------
1 | name: Django CI
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | max-parallel: 4
15 | matrix:
16 | python-version: [3.7, 3.8, 3.9]
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v3
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install Dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install -r requirements.txt
28 | pip install tox
29 | - name: Run Tests
30 | run: |
31 | tox
32 |
--------------------------------------------------------------------------------
/instant/management/commands/runws.py:
--------------------------------------------------------------------------------
1 | import os
2 | import io
3 | import subprocess
4 |
5 | from django.core.management.base import BaseCommand
6 | from instant.conf import CENTRIFUGO_PORT
7 |
8 |
9 | class Command(BaseCommand):
10 | help = "Run the Centrifugo websockets server"
11 |
12 | def handle(self, *args, **options):
13 | basepath = os.getcwd()
14 | c = basepath + "/centrifugo/centrifugo"
15 | conf = basepath + "/centrifugo/config.json"
16 | cmd = [c, "--config", conf, "--port", str(CENTRIFUGO_PORT)]
17 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
18 | for line in io.TextIOWrapper(p.stdout, encoding="utf-8"): # type: ignore
19 | msg = str(line).replace("b'", "")
20 | msg = msg[0:-3]
21 | print(msg)
22 | p.wait()
23 |
--------------------------------------------------------------------------------
/instant/token.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | import time
3 |
4 | from .conf import CENTRIFUGO_HMAC_KEY
5 |
6 | def connection_token(user):
7 | if CENTRIFUGO_HMAC_KEY is None:
8 | raise ValueError("Provide a CENTRIFUGO_HMAC_KEY in settings")
9 | claims = {"sub": user.get_username(), "exp": int(time.time()) + 24 * 60 * 60}
10 | token = jwt.encode(claims, CENTRIFUGO_HMAC_KEY, algorithm="HS256")
11 | return token
12 |
13 |
14 | def channel_token(channel, user):
15 | if CENTRIFUGO_HMAC_KEY is None:
16 | raise ValueError("Provide a CENTRIFUGO_HMAC_KEY in settings")
17 | claims = {
18 | "sub": user.get_username(),
19 | "channel": channel,
20 | "exp": int(time.time()) + 24 * 60 * 60,
21 | }
22 | token = jwt.encode(claims, CENTRIFUGO_HMAC_KEY, algorithm="HS256")
23 | return token
24 |
--------------------------------------------------------------------------------
/instant/tests/base.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from django.test import TestCase
4 | from django.test.client import RequestFactory
5 | from django.contrib.auth.models import User
6 | from django.conf import settings
7 |
8 | from instant.models import Channel
9 |
10 |
11 | class InstantBaseTest(TestCase):
12 | user = None
13 |
14 | def setUp(self):
15 | self.factory = RequestFactory() # type: ignore
16 | self.user = User.objects.create_user( # type: ignore
17 | "myuser", "myemail@test.com", "password"
18 | )
19 | self.superuser = User.objects.create_superuser( # type: ignore
20 | "superuser", "myemail@test.com", "password"
21 | )
22 |
23 | @property
24 | def base_dir(self) -> Path:
25 | d = settings.BASE_DIR
26 | if isinstance(d, str):
27 | d = Path(d)
28 | return d
29 |
30 | def reset(self):
31 | for chan in Channel.objects.all():
32 | chan.delete()
33 |
--------------------------------------------------------------------------------
/instant/tests/test_token.py:
--------------------------------------------------------------------------------
1 | from .base import InstantBaseTest
2 |
3 | from instant.token import connection_token, channel_token
4 |
5 |
6 | class InstantTestToken(InstantBaseTest):
7 | def test_connection_token(self):
8 | #with self.assertRaises(ValueError):
9 | # with self.settings(CENTRIFUGO_HMAC_KEY=None):
10 | # connection_token(self.user)
11 | t = connection_token(self.user)
12 | # print(t)
13 | self.assertTrue(
14 | str(t).startswith(
15 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
16 | )
17 | )
18 |
19 | def test_channel_token(self):
20 | #with self.settings(CENTRIFUGO_HMAC_KEY=None) and self.assertRaises(ValueError):
21 | # channel_token("chan", self.user)
22 | t = channel_token("chan", self.user)
23 | # print(t)
24 | self.assertTrue(
25 | str(t).startswith(
26 | (
27 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
28 | )
29 | )
30 | )
31 |
--------------------------------------------------------------------------------
/instant/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from .base import InstantBaseTest
2 |
3 | from django.urls import reverse
4 | from django.http import JsonResponse, HttpResponseForbidden
5 |
6 |
7 | class InstantTestViews(InstantBaseTest):
8 | def test_logout_view(self):
9 | self.client.login(username="myuser", password="password")
10 | response = self.client.get(reverse("instant-logout"))
11 | self.assertIsInstance(response, JsonResponse)
12 | self.assertEqual(response.status_code, 200)
13 | self.client.logout()
14 | response = self.client.get(reverse("instant-logout"))
15 | self.assertIsInstance(response, HttpResponseForbidden)
16 | self.assertEqual(response.status_code, 403)
17 |
18 | def test_get_token(self):
19 | response = self.client.get(reverse("instant-get-token"))
20 | self.assertIsInstance(response, HttpResponseForbidden)
21 | # self.client.login(username="myuser", password="password")
22 | # response = self.client.get(reverse("instant-get-token"))
23 | # print("RESP", response)
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *node_modules/
2 | settings.py
3 | *.sqlite3
4 | .project
5 | .pydevproject
6 | .settings
7 | coverage_html_report/
8 | test_db
9 | bin/
10 | pyvenv.cfg
11 | .coveragerc
12 | .vscode/
13 | .pytest_cache
14 |
15 | __pycache__/
16 | *.py[cod]
17 |
18 | # C extensions
19 | *.so
20 |
21 | # Distribution / packaging
22 | .Python
23 | env/
24 | build/
25 | develop-eggs/
26 | dist/
27 | downloads/
28 | eggs/
29 | .eggs/
30 | lib/
31 | lib64/
32 | parts/
33 | sdist/
34 | var/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 |
39 | # PyInstaller
40 | # Usually these files are written by a python script from a template
41 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
42 | *.manifest
43 | *.spec
44 |
45 | # Installer logs
46 | pip-log.txt
47 | pip-delete-this-directory.txt
48 |
49 | # Unit test / coverage reports
50 | htmlcov/
51 | .tox/
52 | .coverage
53 | .coverage.*
54 | .cache
55 | nosetests.xml
56 | coverage.xml
57 | *,cover
58 |
59 | # Django stuff:
60 | *.log
61 |
62 | # Sphinx documentation
63 | docs/_build/
64 |
65 | # PyBuilder
66 |
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 synw
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | from os import path
3 |
4 | this_directory = path.abspath(path.dirname(__file__))
5 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
6 | long_description = f.read()
7 |
8 | version = __import__("instant").__version__
9 |
10 | setup(
11 | name="django-instant",
12 | packages=find_packages(),
13 | include_package_data=True,
14 | version=version,
15 | description="Websockets for Django with Centrifugo ",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | author="synw",
19 | author_email="synwe@yahoo.com",
20 | url="https://github.com/synw/django-instant",
21 | download_url="https://github.com/synw/django-instant/releases/tag/" + version,
22 | keywords=["django", "websockets", "centrifugo"],
23 | classifiers=[
24 | "Development Status :: 4 - Beta",
25 | "Framework :: Django :: 4.0",
26 | "Intended Audience :: Developers",
27 | "License :: OSI Approved :: MIT License",
28 | "Programming Language :: Python :: 3.8",
29 | ],
30 | install_requires=["cent", "PyJWT"],
31 | zip_safe=False,
32 | )
33 |
--------------------------------------------------------------------------------
/instant/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations, models
2 |
3 |
4 | class Migration(migrations.Migration):
5 |
6 | initial = True
7 |
8 | operations = [
9 | migrations.CreateModel(
10 | name='Channel',
11 | fields=[
12 | ('id', models.BigAutoField(auto_created=True,
13 | primary_key=True, serialize=False, verbose_name='ID')),
14 | ('name', models.CharField(help_text='Use $ to prefix non public channels: ex: $private_chan',
15 | max_length=120, unique=True, verbose_name='Name')),
16 | ('level', models.CharField(choices=[('public', 'Public'), ('users', 'Users'), ('groups', 'Groups'), (
17 | 'staff', 'Staff'), ('superuser', 'Superuser')], max_length=20, verbose_name='Authorized for')),
18 | ('is_active', models.BooleanField(
19 | default=True, verbose_name='Active')),
20 | ('groups', models.ManyToManyField(
21 | blank=True, to='auth.Group', verbose_name='Groups')),
22 | ],
23 | options={
24 | 'verbose_name': 'Channel',
25 | 'verbose_name_plural': 'Channels',
26 | },
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/instant/tests/test_model.py:
--------------------------------------------------------------------------------
1 | from .base import InstantBaseTest
2 |
3 | from instant.models import Channel
4 |
5 |
6 | class InstantTestCreate(InstantBaseTest):
7 | def test_channels_creation(self):
8 | self.reset()
9 | chan = Channel.objects.create(name="chan")
10 | self.assertTrue(chan.is_active)
11 | self.assertEqual(chan.name, "$chan")
12 | self.assertEqual(chan.level, "superuser")
13 | self.assertEqual(str(chan), "$chan")
14 |
15 | def test_public_channel_creation(self):
16 | chan = Channel.objects.create(name="chan", level="public")
17 | self.assertEqual(chan.name, "chan")
18 | self.assertEqual(chan.level, "public")
19 |
20 | def test_public_channel_creation_enum(self):
21 | chan = Channel.objects.create(name="chan", level=Channel.Level.Public)
22 | self.assertEqual(chan.name, "chan")
23 | self.assertEqual(chan.level, "public")
24 |
25 |
26 | def test_channel_manager(self):
27 | Channel.objects.create(name="$chan")
28 | user_chans = Channel.objects.for_user(self.superuser) # type: ignore
29 | self.assertEqual(user_chans[0].name, "$chan")
30 | Channel.objects.create(name="chan", level="public")
31 | user_chans = Channel.objects.for_user(self.user) # type: ignore
32 | self.assertEqual(user_chans[0].name, "chan")
33 |
--------------------------------------------------------------------------------
/instant/producers.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Any, Dict, Union
3 |
4 | from cent import Client
5 |
6 | from .conf import SITE_NAME, CENTRIFUGO_HOST, CENTRIFUGO_PORT, CENTRIFUGO_API_KEY
7 |
8 |
9 | def publish(
10 | channel: str,
11 | *args,
12 | event_class: Union[str, None] = None,
13 | data: Union[Dict[str, Any], None] = None,
14 | bucket: str = "",
15 | site: str = SITE_NAME
16 | ) -> None:
17 | """
18 | Publish a message to a websockets channel
19 | """
20 | message = None
21 | if len(args) == 1:
22 | message = args[0]
23 | if message is None and data is None:
24 | raise ValueError("Provide either a message or data argument")
25 | cent_url = CENTRIFUGO_HOST
26 | cent_url += ":" + str(CENTRIFUGO_PORT) + "/api"
27 | if CENTRIFUGO_API_KEY is None:
28 | raise ValueError("Provide a CENTRIFUGO_API_KEY in settings")
29 | client = Client(
30 | cent_url,
31 | api_key = CENTRIFUGO_API_KEY,
32 | timeout = 1,
33 | )
34 | payload: Dict[str, Any] = {"channel": channel, "site": site}
35 | if message is not None:
36 | payload["message"] = message
37 | if data is not None:
38 | payload["data"] = data
39 | if event_class is not None:
40 | payload["event_class"] = event_class
41 | if len(bucket) > 0:
42 | payload["bucket"] = bucket
43 | client.publish(channel, json.dumps(payload))
44 |
--------------------------------------------------------------------------------
/instant/init.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, List, Union
2 | from pathlib import Path
3 |
4 | from django.conf import settings
5 |
6 |
7 | def generate_settings_from_conf(
8 | conf: Dict[str, Any], site_name: Union[str, None] = None
9 | ) -> List[str]:
10 | """
11 | Generate Django settings from Centrifugo json conf
12 | """
13 | buffer = []
14 | buffer.append('CENTRIFUGO_HOST = "http://localhost"')
15 | buffer.append("CENTRIFUGO_PORT = 8427")
16 | buffer.append(f'CENTRIFUGO_HMAC_KEY = "{conf["token_hmac_secret_key"]}"')
17 | buffer.append(f'CENTRIFUGO_API_KEY = "{conf["api_key"]}"')
18 | default_base_dir = settings.BASE_DIR
19 | if isinstance(default_base_dir, Path) is False:
20 | default_base_dir = Path(default_base_dir)
21 | project_name = default_base_dir.name
22 | if site_name is not None:
23 | project_name = site_name
24 | buffer.append(f'SITE_NAME = "{project_name}"')
25 | return buffer
26 |
27 |
28 | def ensure_channel_is_private(chan: str) -> str:
29 | """
30 | Make sure that a private channel name starts with a $ sign
31 | """
32 | name = chan
33 | if ":" in name:
34 | names = name.split(":")
35 | prefix = names[0]
36 | suffix = names[1]
37 | if suffix.startswith("$") is False:
38 | return prefix + ":$" + suffix
39 | else:
40 | if chan.startswith("$") is False:
41 | return "$" + chan
42 | return chan
43 |
--------------------------------------------------------------------------------
/instant/tests/test_conf.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 | from .base import InstantBaseTest
3 |
4 | from instant.init import generate_settings_from_conf
5 | from instant.conf import (
6 | CENTRIFUGO_API_KEY,
7 | CENTRIFUGO_HMAC_KEY,
8 | CENTRIFUGO_HOST,
9 | CENTRIFUGO_PORT,
10 | SITE_SLUG,
11 | SITE_NAME,
12 | )
13 |
14 |
15 | class InstantTestConf(InstantBaseTest):
16 | def test_default_conf(self):
17 | self.assertEqual(CENTRIFUGO_API_KEY, "key")
18 | self.assertEqual(CENTRIFUGO_HMAC_KEY, "key")
19 | self.assertEqual(CENTRIFUGO_HOST, "http://localhost")
20 | self.assertEqual(CENTRIFUGO_PORT, 8427)
21 | self.assertEqual(SITE_SLUG, "site")
22 | self.assertEqual(SITE_NAME, "Site")
23 |
24 | def test_generate_settings_from_conf(self):
25 | conf: Dict[str, Any] = {"token_hmac_secret_key": "key", "api_key": "key"}
26 | s = generate_settings_from_conf(conf, "site")
27 | self.assertListEqual(
28 | [
29 | 'CENTRIFUGO_HOST = "http://localhost"',
30 | "CENTRIFUGO_PORT = 8427",
31 | 'CENTRIFUGO_HMAC_KEY = "key"',
32 | 'CENTRIFUGO_API_KEY = "key"',
33 | 'SITE_NAME = "site"',
34 | ],
35 | s,
36 | )
37 | s = generate_settings_from_conf(conf)
38 | self.assertListEqual(
39 | [
40 | 'CENTRIFUGO_HOST = "http://localhost"',
41 | "CENTRIFUGO_PORT = 8427",
42 | 'CENTRIFUGO_HMAC_KEY = "key"',
43 | 'CENTRIFUGO_API_KEY = "key"',
44 | 'SITE_NAME = "tests"',
45 | ],
46 | s,
47 | )
48 |
--------------------------------------------------------------------------------
/instant/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import Group
3 | from django.utils.translation import gettext_lazy as _
4 |
5 | from .init import ensure_channel_is_private
6 |
7 | class ChannelManager(models.Manager):
8 | def for_user(self, user):
9 | filter_from = [Channel.Level.Public, Channel.Level.Users]
10 | if user.is_superuser:
11 | filter_from.append(Channel.Level.Superuser)
12 | if user.is_staff:
13 | filter_from.append(Channel.Level.Staff)
14 | chans = Channel.objects.filter(
15 | is_active=True, groups__in=user.groups.all()
16 | ) | Channel.objects.filter(level__in=filter_from, is_active=True)
17 | return chans
18 |
19 |
20 | class Channel(models.Model):
21 | class Level(models.TextChoices):
22 | Public = "public"
23 | Users = "users"
24 | Groups = "groups"
25 | Staff = "staff"
26 | Superuser = "superuser"
27 |
28 | name = models.CharField(
29 | max_length=120,
30 | verbose_name=_("Name"),
31 | unique=True,
32 | help_text=_("Use $ to prefix non public channels: " "ex: $private_chan"),
33 | )
34 | level = models.CharField(
35 | max_length=20,
36 | choices=Level.choices,
37 | verbose_name=_("Authorized for"),
38 | default="superuser",
39 | )
40 | is_active = models.BooleanField(default=True, verbose_name=_("Active"))
41 | groups = models.ManyToManyField(Group, blank=True, verbose_name=_("Groups"))
42 | objects = ChannelManager()
43 |
44 | class Meta:
45 | verbose_name = _("Channel")
46 | verbose_name_plural = _("Channels")
47 |
48 | def __str__(self) -> str:
49 | return str(self.name)
50 |
51 | def save(self, *args, **kwargs):
52 | # check the channel name
53 | if self.level != "public":
54 | self.name = ensure_channel_is_private(self.__str__())
55 | return super(Channel, self).save(*args, **kwargs)
56 |
--------------------------------------------------------------------------------
/instant/management/commands/installws.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import platform
4 | import subprocess
5 |
6 | from django.core.management.base import BaseCommand
7 |
8 | from instant.init import generate_settings_from_conf
9 |
10 |
11 | class Command(BaseCommand):
12 | help = (
13 | "Install the Centrifugo websockets server for Linux and Darwin"
14 | "and generate the Django settings"
15 | )
16 |
17 | def handle(self, *args, **options):
18 | centrifugo_version = "4.0.4"
19 | run_on = str(platform.system()).lower()
20 | suffix = "_linux_amd64"
21 | if run_on == "darwin":
22 | suffix = "_darwin_amd64"
23 | suffix_file = suffix + ".tar.gz"
24 | fetch_url = (
25 | "https://github.com/centrifugal/centrifugo/releases/download/v"
26 | + centrifugo_version
27 | + "/centrifugo_"
28 | + centrifugo_version
29 | + suffix_file
30 | )
31 | basepath = os.getcwd()
32 | subprocess.call(["mkdir", "centrifugo"])
33 | os.chdir(basepath + "/centrifugo")
34 | subprocess.call(["wget", fetch_url])
35 | name = "centrifugo_" + centrifugo_version + suffix
36 | subprocess.call(["tar", "-xzf", name + ".tar.gz"])
37 | subprocess.call(["rm", "-f", name + ".tar.gz"])
38 | subprocess.call(["chmod", "a+x", "centrifugo"])
39 | subprocess.call(["./centrifugo", "genconfig"])
40 | # set allowed origins in Centrifugo config
41 | filepath = basepath + "/centrifugo/config.json"
42 | with open(filepath, "r+") as f:
43 | content = f.read()
44 | conf = json.loads(content)
45 | conf["allowed_origins"] = [
46 | "http://localhost:8000", # django dev server
47 | "http://localhost:3000", # frontend dev server
48 | "http://localhost:5000", # frontend build preview
49 | ]
50 | output = json.dumps(conf, indent=4)
51 | f.seek(0)
52 | f.write(output)
53 | f.truncate()
54 | f.close()
55 | # generate settings
56 | buffer = generate_settings_from_conf(conf)
57 | # print conf
58 | print("\nAppend this to your Django settings:\n")
59 | for line in buffer:
60 | print(line)
61 | print("\n")
62 | print(
63 | "The Centrifugo websockets server is installed. Run it with python "
64 | "manage.py runws"
65 | )
66 |
--------------------------------------------------------------------------------
/instant/views.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.contrib import auth
4 | from django.http import JsonResponse, HttpResponseForbidden
5 | from django.views.decorators.csrf import csrf_exempt
6 | from django.middleware.csrf import get_token
7 |
8 | from .token import connection_token, channel_token
9 | from .models import Channel
10 |
11 | def user_channels(user):
12 | user_chans = Channel.objects.for_user(user)
13 | authorized_chans = []
14 | for channel in user_chans:
15 | # print("Checking auth for chan", channel, channel in user_chans_names)
16 | chan = {
17 | "name": channel.name,
18 | "level": channel.level,
19 | "token": channel_token(channel.name, user),
20 | }
21 | authorized_chans.append(chan)
22 | return authorized_chans
23 |
24 |
25 | @csrf_exempt # type: ignore
26 | def channels_subscription(request):
27 | if not request.user.is_authenticated:
28 | return HttpResponseForbidden()
29 | json_data = json.loads(request.body)
30 | # print("PRIVATE SUB for", request.user.username, json_data)
31 | user_chans = Channel.objects.for_user(request.user).values("name") # type: ignore
32 | user_chans_names = []
33 | for chan in user_chans:
34 | user_chans_names.append(chan["name"])
35 | authorized_chans = user_channels(request.user)
36 | # print({"channels": authorized_chans})
37 | return JsonResponse({"channels": authorized_chans})
38 |
39 |
40 | def _get_response(request):
41 | channels = user_channels(request.user)
42 | return JsonResponse(
43 | {
44 | "csrf_token": get_token(request),
45 | "ws_token": connection_token(request.user),
46 | "channels": channels,
47 | }
48 | )
49 |
50 |
51 | @csrf_exempt # type: ignore
52 | def login_and_get_tokens(request):
53 | # print("Login view", request.method)
54 | if request.user.is_authenticated:
55 | return _get_response(request)
56 | if request.method == "POST":
57 | # print("POST", request.body)
58 | json_data = json.loads(request.body)
59 | username = json_data["username"]
60 | password = json_data["password"]
61 | # print("Authenticate", username, password)
62 | user = auth.authenticate(username=username, password=password)
63 | if user is not None:
64 | auth.login(request, user)
65 | return _get_response(request)
66 | return HttpResponseForbidden()
67 |
68 |
69 | def logout(request):
70 | if request.user.is_authenticated:
71 | auth.logout(request)
72 | return JsonResponse({"response": "ok"})
73 | return HttpResponseForbidden()
74 |
75 |
76 | @csrf_exempt # type: ignore
77 | def get_connection_token(request):
78 | if not request.user.is_authenticated:
79 | return HttpResponseForbidden()
80 | return _get_response(request)
81 |
--------------------------------------------------------------------------------
/instant/tests/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import sys
4 | from pathlib import Path
5 |
6 | import django
7 | from django.conf import settings
8 | from django.core.management import execute_from_command_line
9 |
10 |
11 | BASE_DIR = Path(os.path.dirname(__file__))
12 | sys.path.append(BASE_DIR.parent.resolve().as_posix())
13 |
14 | # Unfortunately, apps can not be installed via ``modify_settings``
15 | # decorator, because it would miss the database setup.
16 | CUSTOM_INSTALLED_APPS = ("django.contrib.admin", "instant")
17 |
18 | ALWAYS_INSTALLED_APPS = (
19 | "django.contrib.auth",
20 | "django.contrib.contenttypes",
21 | "django.contrib.sessions",
22 | "django.contrib.messages",
23 | "django.contrib.staticfiles",
24 | )
25 |
26 | ALWAYS_MIDDLEWARE_CLASSES = (
27 | "django.contrib.sessions.middleware.SessionMiddleware",
28 | "django.middleware.common.CommonMiddleware",
29 | "django.middleware.csrf.CsrfViewMiddleware",
30 | "django.contrib.auth.middleware.AuthenticationMiddleware",
31 | "django.contrib.messages.middleware.MessageMiddleware",
32 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
33 | )
34 |
35 | TEMPLATES_DIR = BASE_DIR / "templates"
36 |
37 | TEMPLATES = [
38 | {
39 | "BACKEND": "django.template.backends.django.DjangoTemplates",
40 | "DIRS": [TEMPLATES_DIR],
41 | "APP_DIRS": True,
42 | "OPTIONS": {
43 | "context_processors": [
44 | "django.template.context_processors.debug",
45 | "django.template.context_processors.request",
46 | "django.contrib.auth.context_processors.auth",
47 | "django.contrib.messages.context_processors.messages",
48 | ],
49 | },
50 | },
51 | ]
52 |
53 |
54 | settings.configure(
55 | BASE_DIR=BASE_DIR,
56 | SECRET_KEY="django_tests_secret_key",
57 | DEBUG=False,
58 | TEMPLATES=TEMPLATES,
59 | TEMPLATE_DEBUG=False,
60 | ALLOWED_HOSTS=[],
61 | INSTALLED_APPS=ALWAYS_INSTALLED_APPS + CUSTOM_INSTALLED_APPS,
62 | MIDDLEWARE=ALWAYS_MIDDLEWARE_CLASSES,
63 | ROOT_URLCONF="tests.urls",
64 | DATABASES={
65 | "default": {
66 | "ENGINE": "django.db.backends.sqlite3",
67 | "NAME": "test_db",
68 | }
69 | },
70 | LANGUAGE_CODE="en-us",
71 | TIME_ZONE="UTC",
72 | USE_I18N=True,
73 | USE_L10N=True,
74 | USE_TZ=True,
75 | STATIC_URL="/static/",
76 | # Use a fast hasher to speed up tests.
77 | PASSWORD_HASHERS=("django.contrib.auth.hashers.MD5PasswordHasher",),
78 | FIXTURE_DIRS=BASE_DIR / "*/fixtures/",
79 | MQUEUE_AUTOREGISTER=(("django.contrib.auth.models.User", ["c", "d", "u"]),),
80 | MQUEUE_HOOKS={
81 | "redis": {
82 | "path": "mqueue.hooks.redis",
83 | "host": "localhost",
84 | "port": 6379,
85 | "db": 0,
86 | },
87 | },
88 | DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
89 | CENTRIFUGO_API_KEY="key",
90 | CENTRIFUGO_HMAC_KEY="key",
91 | )
92 |
93 | django.setup()
94 | args = [sys.argv[0], "test"]
95 |
96 | # Current module (``tests``) and its submodules.
97 | test_cases = "."
98 |
99 | # Allow accessing test options from the command line.
100 | offset = 1
101 | try:
102 | sys.argv[1]
103 | except IndexError:
104 | pass
105 | else:
106 | option = sys.argv[1].startswith("-")
107 | if not option:
108 | test_cases = sys.argv[1]
109 | offset = 2
110 |
111 | args.append(test_cases)
112 | # ``verbosity`` can be overwritten from command line.
113 | args.append("--verbosity=2")
114 | args.extend(sys.argv[offset:])
115 |
116 | execute_from_command_line(args)
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django Instant
2 |
3 | [](https://pypi.org/project/django-instant/) [](https://github.com/synw/django-instant/actions/workflows/django.yml)
4 |
5 | Websockets for Django with [Centrifugo](https://github.com/centrifugal/centrifugo).
6 |
7 | * Push events into public or private channels.
8 | * Handle the events in javascript client-side.
9 |
10 | :sunny: Compatible: plug it on an existing Django instance _without any modification in your main stack_
11 |
12 | ### Example
13 |
14 | Push events in channels from anywhere in the code:
15 |
16 | ```python
17 | from instant.producers import publish
18 |
19 | # Publish to a public channel
20 | publish("public", "Message for everyone")
21 |
22 | # Publish to a private channel with an event class set
23 | publish("$users", "Message in logged in users channel", event_class="important")
24 |
25 | # Publish to a group channel
26 | publish("$group1", "Message for users in group1")
27 |
28 | # Publish to the staff channel with an extra json data payload
29 | data = {"field1":"value1","field2":[1,2]}
30 | publish("$staff", "Message for staff", data=data)
31 | ```
32 |
33 | ## Quick start
34 |
35 | ### Install the Django package
36 |
37 | ```
38 | pip install django-instant
39 | ```
40 |
41 | Add `"instant"` to `INSTALLED_APPS` and update `urls.py`:
42 |
43 | ```python
44 | urlpatterns = [
45 | # ...
46 | path("instant/", include("instant.urls")),
47 | ]
48 | ```
49 |
50 | ### Install the websockets server
51 |
52 | #### Using the installer
53 |
54 | Use the Centrifugo installer management command (for Linux and MacOs):
55 |
56 | ```
57 | python manage.py installws
58 | ```
59 |
60 | This will download a Centrifugo binary release and install it under a *centrifugo* directory. It will
61 | generate the Django settings to use.
62 |
63 | #### Install manualy
64 |
65 | Install the Centrifugo websockets server: see the [detailled doc](https://centrifugal.github.io/centrifugo/server/install/)
66 |
67 |
68 |
69 | Download a release https://github.com/centrifugal/centrifugo/releases/latest
70 | and generate a configuration file:
71 |
72 | ```
73 | ./centrifugo genconfig
74 | ```
75 |
76 | The generated `config.json` file looks like this:
77 |
78 | ```javascript
79 | {
80 | "v3_use_offset": true,
81 | "token_hmac_secret_key": "46b38493-147e-4e3f-86e0-dc5ec54f5133",
82 | "admin_password": "ad0dff75-3131-4a02-8d64-9279b4f1c57b",
83 | "admin_secret": "583bc4b7-0fa5-4c4a-8566-16d3ce4ad401",
84 | "api_key": "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6",
85 | "allowed_origins": []
86 | }
87 | ```
88 |
89 |
90 | ### Configure the Django settings
91 |
92 | Use the parameters from the installer's output or from Centrifugo's `config.json` file
93 | to update your Django's `settings.py`:
94 |
95 | ```python
96 | CENTRIFUGO_HOST = "http://localhost"
97 | CENTRIFUGO_PORT = 8001
98 | CENTRIFUGO_HMAC_KEY = "46b38493-147e-4e3f-86e0-dc5ec54f5133"
99 | CENTRIFUGO_API_KEY = "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6"
100 | SITE_NAME = "My site" # used in the messages to identify where they come from
101 | ```
102 |
103 | ### Create channels
104 |
105 | Go into the admin to create channels or create them programatically:
106 |
107 | ```python
108 | from instant.models import Channel
109 |
110 | Channel.objects.create(name="superuser", level=Channel.Level.Superuser)
111 | ```
112 |
113 | Api: channel create parameters:
114 |
115 | - `name`: the channel name. Required and unique
116 | - `level`: access authorization level for a channel: *Public, Users, Groups, Staff, Superuser*. Default: *Superuser*
117 | - `is_active`: a boolean to disable a channel
118 | - `groups`: a list of authorized Django groups for a channel
119 |
120 | ## Avalailable endpoints
121 |
122 | `/instant/login/`: takes a username and password as parameter and will login the
123 | user in Django and return a Centrifugo connection token
124 |
125 | `/instant/get_token/`: get a Centrifugo connection token for a logged in user
126 |
127 | The two methods above return some connection information: a token for
128 | the websockets connection, a Django csrf token and a list of authorized
129 | channels for the user:
130 |
131 | ```javascript
132 | {
133 | "csrf_token": "fvO61oyhcfzrW3SjPCYxYfzDAQFO6Yz7yaAQkxDbhC0NhlwoP1cecqLEYv8SCDLK",
134 | "ws_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJnZ2ciLCJleHAiOjE2M..",
135 | "channels": [
136 | {
137 | "name": "public",
138 | "level": "public"
139 | },
140 | {
141 | "name": "$users",
142 | "level": "users"
143 | },
144 | {
145 | "name": "$group1",
146 | "level": "groups"
147 | }
148 | ]
149 | }
150 | ```
151 |
152 | `/instant/subscribe/`: get tokens for Centrifugo channels subscriptions
153 | ([doc](https://centrifugal.github.io/centrifugo/server/private_channels/))
154 |
155 | ## Publish method
156 |
157 | The required parameters are `channel` and either `message` or `data`
158 |
159 | ```python
160 | publish("$users", "A message", data={
161 | "foo": "bar"}, event_class="important", bucket="notifications")
162 | ```
163 |
164 | The other parameters are optional
165 |
166 | ## Javascript client
167 |
168 | Several options are available for the client side
169 |
170 | ### Use the official Centrifugo js client
171 |
172 | Manage your websockets connection manually with the official Centrifugo js library:
173 | [Centrifuge-js](https://github.com/centrifugal/centrifuge-js)
174 |
175 | ### Use the embeded client in a script tag
176 |
177 | In a Django template:
178 |
179 | ```html
180 | {% load static %}
181 |
182 |
185 | ```
186 |
187 | [Api doc](https://github.com/synw/djangoinstant#usage)
188 |
189 | ### Use the npm client
190 |
191 | A dedicated [client](https://github.com/synw/djangoinstant) is available from npm
192 | to handle the messages and connections client side in javascript or typescript
193 |
194 | ## Example
195 |
196 | An [example](https://github.com/synw/django-instant-example) with a backend and a frontend is available
197 |
198 | ## Tests
199 |
200 | To run the tests:
201 |
202 | ```bash
203 | tox
204 | ```
205 |
--------------------------------------------------------------------------------
/instant/static/instant/index.min.js:
--------------------------------------------------------------------------------
1 | var $instant=function(e){"use strict";var t,s="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n={},i={},r={},o={exports:{}},c="object"==typeof Reflect?Reflect:null,a=c&&"function"==typeof c.apply?c.apply:function(e,t,s){return Function.prototype.apply.call(e,t,s)};t=c&&"function"==typeof c.ownKeys?c.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var h=Number.isNaN||function(e){return e!=e};function u(){u.init.call(this)}o.exports=u,o.exports.once=function(e,t){return new Promise((function(s,n){function i(s){e.removeListener(t,r),n(s)}function r(){"function"==typeof e.removeListener&&e.removeListener("error",i),s([].slice.call(arguments))}S(e,t,r,{once:!0}),"error"!==t&&function(e,t,s){"function"==typeof e.on&&S(e,"error",t,s)}(e,i,{once:!0})}))},u.EventEmitter=u,u.prototype._events=void 0,u.prototype._eventsCount=0,u.prototype._maxListeners=void 0;var l=10;function d(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function _(e){return void 0===e._maxListeners?u.defaultMaxListeners:e._maxListeners}function p(e,t,s,n){var i,r,o,c;if(d(s),void 0===(r=e._events)?(r=e._events=Object.create(null),e._eventsCount=0):(void 0!==r.newListener&&(e.emit("newListener",t,s.listener?s.listener:s),r=e._events),o=r[t]),void 0===o)o=r[t]=s,++e._eventsCount;else if("function"==typeof o?o=r[t]=n?[s,o]:[o,s]:n?o.unshift(s):o.push(s),(i=_(e))>0&&o.length>i&&!o.warned){o.warned=!0;var a=new Error("Possible EventEmitter memory leak detected. "+o.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");a.name="MaxListenersExceededWarning",a.emitter=e,a.type=t,a.count=o.length,c=a,console&&console.warn&&console.warn(c)}return e}function b(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function f(e,t,s){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:s},i=b.bind(n);return i.listener=s,n.wrapFn=i,i}function m(e,t,s){var n=e._events;if(void 0===n)return[];var i=n[t];return void 0===i?[]:"function"==typeof i?s?[i.listener||i]:[i]:s?function(e){for(var t=new Array(e.length),s=0;s0&&(r=t[0]),r instanceof Error)throw r;var o=new Error("Unhandled error."+(r?" ("+r.message+")":""));throw o.context=r,o}var c=i[e];if(void 0===c)return!1;if("function"==typeof c)a(c,this,t);else{var h=c.length,u=v(c,h);for(s=0;s=0;r--)if(s[r]===t||s[r].listener===t){o=s[r].listener,i=r;break}if(i<0)return this;0===i?s.shift():function(e,t){for(;t+1=0;n--)this.removeListener(e,t[n]);return this},u.prototype.listeners=function(e){return m(this,e,!0)},u.prototype.rawListeners=function(e){return m(this,e,!1)},u.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):g.call(e,t)},u.prototype.listenerCount=g,u.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]};var y={};Object.defineProperty(y,"__esModule",{value:!0}),y.unsubscribedCodes=y.subscribingCodes=y.disconnectedCodes=y.connectingCodes=y.errorCodes=void 0,y.errorCodes={timeout:1,transportClosed:2,clientDisconnected:3,clientClosed:4,clientConnectToken:5,clientRefreshToken:6,subscriptionUnsubscribed:7,subscriptionSubscribeToken:8,subscriptionRefreshToken:9,transportWriteError:10,connectionClosed:11},y.connectingCodes={connectCalled:0,transportClosed:1,noPing:2,subscribeTimeout:3,unsubscribeError:4},y.disconnectedCodes={disconnectCalled:0,unauthorized:1,badProtocol:2,messageSizeLimit:3},y.subscribingCodes={subscribeCalled:0,transportClosed:1},y.unsubscribedCodes={unsubscribeCalled:0,unauthorized:1,clientClosed:2};var T={};!function(e){var t,s;Object.defineProperty(e,"__esModule",{value:!0}),e.SubscriptionState=e.State=void 0,(t=e.State||(e.State={})).Disconnected="disconnected",t.Connecting="connecting",t.Connected="connected",(s=e.SubscriptionState||(e.SubscriptionState={})).Unsubscribed="unsubscribed",s.Subscribing="subscribing",s.Subscribed="subscribed"}(T);var w={};function C(e){return null!=e&&"function"==typeof e}Object.defineProperty(w,"__esModule",{value:!0}),w.ttlMilliseconds=w.errorExists=w.backoff=w.log=w.isFunction=w.startsWith=void 0,w.startsWith=function(e,t){return 0===e.lastIndexOf(t,0)},w.isFunction=C,w.log=function(e,t){if(globalThis.console){const s=globalThis.console[e];C(s)&&s.apply(globalThis.console,t)}},w.backoff=function(e,t,s){e>31&&(e=31);const n=function(e,t){return Math.floor(Math.random()*(t-e+1)+e)}(0,Math.min(s,t*Math.pow(2,e)));return Math.min(s,t+n)},w.errorExists=function(e){return"error"in e&&null!==e.error},w.ttlMilliseconds=function(e){return Math.min(1e3*e,2147483647)};var k=s&&s.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.Subscription=void 0;const P=k(o.exports),E=y,x=T,R=w;class j extends P.default{constructor(e,t,s){super(),this._resubscribeTimeout=null,this._refreshTimeout=null,this.channel=t,this.state=x.SubscriptionState.Unsubscribed,this._centrifuge=e,this._token=null,this._getToken=null,this._data=null,this._recover=!1,this._offset=null,this._epoch=null,this._recoverable=!1,this._positioned=!1,this._joinLeave=!1,this._minResubscribeDelay=500,this._maxResubscribeDelay=2e4,this._resubscribeTimeout=null,this._resubscribeAttempts=0,this._promises={},this._promiseId=0,this._inflight=!1,this._refreshTimeout=null,this._setOptions(s),this._centrifuge._debugEnabled?(this.on("state",(e=>{this._centrifuge._debug("subscription state",t,e.oldState,"->",e.newState)})),this.on("error",(e=>{this._centrifuge._debug("subscription error",t,e)}))):this.on("error",(function(){Function.prototype()}))}ready(e){return this.state===x.SubscriptionState.Unsubscribed?Promise.reject({code:E.errorCodes.subscriptionUnsubscribed,message:this.state}):this.state===x.SubscriptionState.Subscribed?Promise.resolve():new Promise(((t,s)=>{const n={resolve:t,reject:s};e&&(n.timeout=setTimeout((function(){s({code:E.errorCodes.timeout,message:"timeout"})}),e)),this._promises[this._nextPromiseId()]=n}))}subscribe(){this._isSubscribed()||(this._resubscribeAttempts=0,this._setSubscribing(E.subscribingCodes.subscribeCalled,"subscribe called"))}unsubscribe(){this._setUnsubscribed(E.unsubscribedCodes.unsubscribeCalled,"unsubscribe called",!0)}publish(e){const t=this;return this._methodCall().then((function(){return t._centrifuge.publish(t.channel,e)}))}presence(){const e=this;return this._methodCall().then((function(){return e._centrifuge.presence(e.channel)}))}presenceStats(){const e=this;return this._methodCall().then((function(){return e._centrifuge.presenceStats(e.channel)}))}history(e){const t=this;return this._methodCall().then((function(){return t._centrifuge.history(t.channel,e)}))}_methodCall(){return this._isSubscribed()?Promise.resolve():this._isUnsubscribed()?Promise.reject({code:E.errorCodes.subscriptionUnsubscribed,message:this.state}):new Promise(((e,t)=>{const s=setTimeout((function(){t({code:E.errorCodes.timeout,message:"timeout"})}),this._centrifuge._config.timeout);this._promises[this._nextPromiseId()]={timeout:s,resolve:e,reject:t}}))}_nextPromiseId(){return++this._promiseId}_needRecover(){return!0===this._recover}_isUnsubscribed(){return this.state===x.SubscriptionState.Unsubscribed}_isSubscribing(){return this.state===x.SubscriptionState.Subscribing}_isSubscribed(){return this.state===x.SubscriptionState.Subscribed}_setState(e){if(this.state!==e){const t=this.state;return this.state=e,this.emit("state",{newState:e,oldState:t,channel:this.channel}),!0}return!1}_usesToken(){return null!==this._token||null!==this._getToken}_clearSubscribingState(){this._resubscribeAttempts=0,this._clearResubscribeTimeout()}_clearSubscribedState(){this._clearRefreshTimeout()}_setSubscribed(e){if(!this._isSubscribing())return;this._clearSubscribingState(),e.recoverable&&(this._recover=!0,this._offset=e.offset||0,this._epoch=e.epoch||""),this._setState(x.SubscriptionState.Subscribed);const t=this._centrifuge._getSubscribeContext(this.channel,e);this.emit("subscribed",t),this._resolvePromises();const s=e.publications;if(s&&s.length>0)for(const e in s)s.hasOwnProperty(e)&&this._handlePublication(s[e]);!0===e.expires&&(this._refreshTimeout=setTimeout((()=>this._refresh()),(0,R.ttlMilliseconds)(e.ttl)))}_setSubscribing(e,t){this._isSubscribing()||(this._isSubscribed()&&this._clearSubscribedState(),this._setState(x.SubscriptionState.Subscribing)&&this.emit("subscribing",{channel:this.channel,code:e,reason:t}),this._subscribe(!1,!1))}_subscribe(e,t){if(this._centrifuge._debug("subscribing on",this.channel),this._centrifuge.state!==x.State.Connected&&!e)return this._centrifuge._debug("delay subscribe on",this.channel,"till connected"),null;if(this._usesToken()){if(this._token)return this._sendSubscribe(this._token,t);{if(e)return null;const t=this;return this._getSubscriptionToken().then((function(e){t._isSubscribing()&&(e?(t._token=e,t._sendSubscribe(e,!1)):t._failUnauthorized())})).catch((function(e){t._isSubscribing()&&(t.emit("error",{type:"subscribeToken",channel:t.channel,error:{code:E.errorCodes.subscriptionSubscribeToken,message:void 0!==e?e.toString():""}}),t._scheduleResubscribe())})),null}}return this._sendSubscribe("",t)}_sendSubscribe(e,t){const s={channel:this.channel};if(e&&(s.token=e),this._data&&(s.data=this._data),this._positioned&&(s.positioned=!0),this._recoverable&&(s.recoverable=!0),this._joinLeave&&(s.join_leave=!0),this._needRecover()){s.recover=!0;const e=this._getOffset();e&&(s.offset=e);const t=this._getEpoch();t&&(s.epoch=t)}const n={subscribe:s};return this._inflight=!0,this._centrifuge._call(n,t).then((e=>{this._inflight=!1;const t=e.reply.subscribe;this._handleSubscribeResponse(t),e.next&&e.next()}),(e=>{this._inflight=!1,this._handleSubscribeError(e.error),e.next&&e.next()})),n}_handleSubscribeError(e){this._isSubscribing()&&(e.code!==E.errorCodes.timeout?this._subscribeError(e):this._centrifuge._disconnect(E.connectingCodes.subscribeTimeout,"subscribe timeout",!0))}_handleSubscribeResponse(e){this._isSubscribing()&&this._setSubscribed(e)}_setUnsubscribed(e,t,s){this._isUnsubscribed()||(this._isSubscribed()&&(s&&this._centrifuge._unsubscribe(this),this._clearSubscribedState()),this._isSubscribing()&&this._clearSubscribingState(),this._setState(x.SubscriptionState.Unsubscribed)&&this.emit("unsubscribed",{channel:this.channel,code:e,reason:t}),this._rejectPromises({code:E.errorCodes.subscriptionUnsubscribed,message:this.state}))}_handlePublication(e){const t=this._centrifuge._getPublicationContext(this.channel,e);this.emit("publication",t),e.offset&&(this._offset=e.offset)}_handleJoin(e){const t=this._centrifuge._getJoinLeaveContext(e.info);this.emit("join",{channel:this.channel,info:t})}_handleLeave(e){const t=this._centrifuge._getJoinLeaveContext(e.info);this.emit("leave",{channel:this.channel,info:t})}_resolvePromises(){for(const e in this._promises)this._promises[e].timeout&&clearTimeout(this._promises[e].timeout),this._promises[e].resolve(),delete this._promises[e]}_rejectPromises(e){for(const t in this._promises)this._promises[t].timeout&&clearTimeout(this._promises[t].timeout),this._promises[t].reject(e),delete this._promises[t]}_scheduleResubscribe(){const e=this,t=this._getResubscribeDelay();this._resubscribeTimeout=setTimeout((function(){e._isSubscribing()&&e._subscribe(!1,!1)}),t)}_subscribeError(e){if(this._isSubscribing())if(e.code<100||109===e.code||!0===e.temporary){109===e.code&&(this._token=null);const t={channel:this.channel,type:"subscribe",error:e};this._centrifuge.state===x.State.Connected&&this.emit("error",t),this._scheduleResubscribe()}else this._setUnsubscribed(e.code,e.message,!1)}_getResubscribeDelay(){const e=(0,R.backoff)(this._resubscribeAttempts,this._minResubscribeDelay,this._maxResubscribeDelay);return this._resubscribeAttempts++,e}_setOptions(e){e&&(e.since&&(this._offset=e.since.offset,this._epoch=e.since.epoch,this._recover=!0),e.data&&(this._data=e.data),void 0!==e.minResubscribeDelay&&(this._minResubscribeDelay=e.minResubscribeDelay),void 0!==e.maxResubscribeDelay&&(this._maxResubscribeDelay=e.maxResubscribeDelay),e.token&&(this._token=e.token),e.getToken&&(this._getToken=e.getToken),!0===e.positioned&&(this._positioned=!0),!0===e.recoverable&&(this._recoverable=!0),!0===e.joinLeave&&(this._joinLeave=!0))}_getOffset(){const e=this._offset;return null!==e?e:0}_getEpoch(){const e=this._epoch;return null!==e?e:""}_clearRefreshTimeout(){null!==this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null)}_clearResubscribeTimeout(){null!==this._resubscribeTimeout&&(clearTimeout(this._resubscribeTimeout),this._resubscribeTimeout=null)}_getSubscriptionToken(){this._centrifuge._debug("get subscription token for channel",this.channel);const e={channel:this.channel},t=this._getToken;if(null===t)throw new Error("provide a function to get channel subscription token");return t(e)}_refresh(){this._clearRefreshTimeout();const e=this;this._getSubscriptionToken().then((function(t){if(!e._isSubscribed())return;if(!t)return void e._failUnauthorized();e._token=t;const s={sub_refresh:{channel:e.channel,token:t}};e._centrifuge._call(s).then((t=>{const s=t.reply.sub_refresh;e._refreshResponse(s),t.next&&t.next()}),(t=>{e._refreshError(t.error),t.next&&t.next()}))})).catch((function(t){e.emit("error",{type:"refreshToken",channel:e.channel,error:{code:E.errorCodes.subscriptionRefreshToken,message:void 0!==t?t.toString():""}}),e._refreshTimeout=setTimeout((()=>e._refresh()),e._getRefreshRetryDelay())}))}_refreshResponse(e){this._isSubscribed()&&(this._centrifuge._debug("subscription token refreshed, channel",this.channel),this._clearRefreshTimeout(),!0===e.expires&&(this._refreshTimeout=setTimeout((()=>this._refresh()),(0,R.ttlMilliseconds)(e.ttl))))}_refreshError(e){this._isSubscribed()&&(e.code<100||!0===e.temporary?(this.emit("error",{type:"refresh",channel:this.channel,error:e}),this._refreshTimeout=setTimeout((()=>this._refresh()),this._getRefreshRetryDelay())):this._setUnsubscribed(e.code,e.message,!0))}_getRefreshRetryDelay(){return(0,R.backoff)(0,1e4,2e4)}_failUnauthorized(){this._setUnsubscribed(E.unsubscribedCodes.unauthorized,"unauthorized",!0)}}r.Subscription=j;var O={};Object.defineProperty(O,"__esModule",{value:!0}),O.SockjsTransport=void 0;O.SockjsTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._transport=null}name(){return"sockjs"}subName(){return"sockjs-"+this._transport.transport}emulation(){return!1}supported(){return null!==this.options.sockjs}initialize(e,t){this._transport=new this.options.sockjs(this.endpoint,null,this.options.sockjsOptions),this._transport.onopen=()=>{t.onOpen()},this._transport.onerror=e=>{t.onError(e)},this._transport.onclose=e=>{t.onClose(e)},this._transport.onmessage=e=>{t.onMessage(e.data)}}close(){this._transport.close()}send(e){this._transport.send(e)}};var L={};Object.defineProperty(L,"__esModule",{value:!0}),L.WebsocketTransport=void 0;L.WebsocketTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._transport=null}name(){return"websocket"}subName(){return"websocket"}emulation(){return!1}supported(){return void 0!==this.options.websocket&&null!==this.options.websocket}initialize(e,t){let s="";"protobuf"===e&&(s="centrifuge-protobuf"),this._transport=""!==s?new this.options.websocket(this.endpoint,s):new this.options.websocket(this.endpoint),"protobuf"===e&&(this._transport.binaryType="arraybuffer"),this._transport.onopen=()=>{t.onOpen()},this._transport.onerror=e=>{t.onError(e)},this._transport.onclose=e=>{t.onClose(e)},this._transport.onmessage=e=>{t.onMessage(e.data)}}close(){this._transport.close()}send(e){this._transport.send(e)}};var D={};Object.defineProperty(D,"__esModule",{value:!0}),D.HttpStreamTransport=void 0;D.HttpStreamTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._abortController=null,this._utf8decoder=new TextDecoder,this._protocol="json"}name(){return"http_stream"}subName(){return"http_stream"}emulation(){return!0}_handleErrors(e){if(!e.ok)throw new Error(e.status);return e}_fetchEventTarget(e,t,s){const n=new EventTarget;return(0,e.options.fetch)(t,s).then(e._handleErrors).then((t=>{n.dispatchEvent(new Event("open"));let s="",i=0,r=new Uint8Array;const o=t.body.getReader();return new e.options.readableStream({start:t=>function c(){return o.read().then((({done:o,value:a})=>{if(o)return n.dispatchEvent(new Event("close")),void t.close();try{if("json"===e._protocol)for(s+=e._utf8decoder.decode(a);i{n.dispatchEvent(new Event("error",{detail:e})),n.dispatchEvent(new Event("close"))})),n}supported(){return null!==this.options.fetch&&null!==this.options.readableStream&&"undefined"!=typeof TextDecoder&&"undefined"!=typeof AbortController&&"undefined"!=typeof EventTarget&&"undefined"!=typeof Event&&"undefined"!=typeof MessageEvent&&"undefined"!=typeof Error}initialize(e,t,s){let n,i;this._protocol=e,this._abortController=new AbortController,"json"===e?(n={Accept:"application/json","Content-Type":"application/json"},i=s):(n={Accept:"application/octet-stream","Content-Type":"application/octet-stream"},i=s);const r={method:"POST",headers:n,body:i,mode:"cors",credentials:"same-origin",cache:"no-cache",signal:this._abortController.signal},o=this._fetchEventTarget(this,this.endpoint,r);o.addEventListener("open",(()=>{t.onOpen()})),o.addEventListener("error",(e=>{this._abortController.abort(),t.onError(e)})),o.addEventListener("close",(()=>{this._abortController.abort(),t.onClose({code:4,reason:"connection closed"})})),o.addEventListener("message",(e=>{t.onMessage(e.data)}))}close(){this._abortController.abort()}send(e,t,s){let n,i;const r={session:t,node:s,data:e};"json"===this._protocol?(n={"Content-Type":"application/json"},i=JSON.stringify(r)):(n={"Content-Type":"application/octet-stream"},i=this.options.encoder.encodeEmulationRequest(r));const o={method:"POST",headers:n,body:i,mode:"cors",credentials:"same-origin",cache:"no-cache"};(0,this.options.fetch)(this.options.emulationEndpoint,o)}};var M={};Object.defineProperty(M,"__esModule",{value:!0}),M.SseTransport=void 0;M.SseTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._protocol="json",this._transport=null,this._onClose=null}name(){return"sse"}subName(){return"sse"}emulation(){return!0}supported(){return null!==this.options.eventsource&&null!==this.options.fetch}initialize(e,t,s){let n;n=globalThis&&globalThis.document&&globalThis.document.baseURI?new URL(this.endpoint,globalThis.document.baseURI):new URL(this.endpoint),n.searchParams.append("cf_connect",s);const i=new this.options.eventsource(n.toString(),{});this._transport=i;i.onopen=function(){t.onOpen()},i.onerror=function(e){i.close(),t.onError(e),t.onClose({code:4,reason:"connection closed"})},i.onmessage=function(e){t.onMessage(e.data)},this._onClose=function(){t.onClose({code:4,reason:"connection closed"})}}close(){this._transport.close(),null!==this._onClose&&this._onClose()}send(e,t,s){const n={session:t,node:s,data:e},i={method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n),mode:"cors",credentials:"same-origin",cache:"no-cache"};(0,this.options.fetch)(this.options.emulationEndpoint,i)}};var U={},I=s&&s.__awaiter||function(e,t,s,n){return new(s||(s=Promise))((function(i,r){function o(e){try{a(n.next(e))}catch(e){r(e)}}function c(e){try{a(n.throw(e))}catch(e){r(e)}}function a(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(o,c)}a((n=n.apply(e,t||[])).next())}))};Object.defineProperty(U,"__esModule",{value:!0}),U.WebtransportTransport=void 0;U.WebtransportTransport=class{constructor(e,t){this.endpoint=e,this.options=t,this._transport=null,this._stream=null,this._writer=null,this._utf8decoder=new TextDecoder,this._protocol="json"}name(){return"webtransport"}subName(){return"webtransport"}emulation(){return!1}supported(){return void 0!==this.options.webtransport&&null!==this.options.webtransport}initialize(e,t){return I(this,void 0,void 0,(function*(){let s;s=globalThis&&globalThis.document&&globalThis.document.baseURI?new URL(this.endpoint,globalThis.document.baseURI):new URL(this.endpoint),"protobuf"===e&&s.searchParams.append("cf_protocol","protobuf"),this._protocol=e;const n=new EventTarget;this._transport=new this.options.webtransport(s.toString()),this._transport.closed.then((()=>{t.onClose({code:4,reason:"connection closed"})})).catch((()=>{t.onClose({code:4,reason:"connection closed"})}));try{yield this._transport.ready}catch(e){return void this.close()}let i;try{i=yield this._transport.createBidirectionalStream()}catch(e){return void this.close()}this._stream=i,this._writer=this._stream.writable.getWriter(),n.addEventListener("close",(()=>{t.onClose({code:4,reason:"connection closed"})})),n.addEventListener("message",(e=>{t.onMessage(e.data)})),this._startReading(n),t.onOpen()}))}_startReading(e){return I(this,void 0,void 0,(function*(){const t=this._stream.readable.getReader();let s="",n=0,i=new Uint8Array;try{for(;;){const{done:r,value:o}=yield t.read();if(o.length>0)if("json"===this._protocol)for(s+=this._utf8decoder.decode(o);nJSON.stringify(e))).join("\n")}};W.JsonDecoder=class{decodeReplies(e){return e.trim().split("\n").map((e=>JSON.parse(e)))}};var A=s&&s.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(i,"__esModule",{value:!0}),i.Centrifuge=void 0;const J=r,z=y,N=O,F=L,q=D,B=M,H=U,$=W,K=w,X=T,G=A(o.exports),Q={protocol:"json",token:null,getToken:null,data:null,debug:!1,name:"js",version:"",fetch:null,readableStream:null,websocket:null,eventsource:null,sockjs:null,sockjsOptions:{},emulationEndpoint:"/emulation",minReconnectDelay:500,maxReconnectDelay:2e4,timeout:5e3,maxServerPingDelay:1e4};class V extends G.default{constructor(e,t){super(),this._reconnectTimeout=null,this._refreshTimeout=null,this._serverPingTimeout=null,this.state=X.State.Disconnected,this._endpoint=e,this._emulation=!1,this._transports=[],this._currentTransportIndex=0,this._triedAllTransports=!1,this._transportWasOpen=!1,this._transport=null,this._transportClosed=!0,this._encoder=null,this._decoder=null,this._reconnectTimeout=null,this._reconnectAttempts=0,this._client=null,this._session="",this._node="",this._subs={},this._serverSubs={},this._commandId=0,this._commands=[],this._batching=!1,this._refreshRequired=!1,this._refreshTimeout=null,this._callbacks={},this._token=void 0,this._dispatchPromise=Promise.resolve(),this._serverPing=0,this._serverPingTimeout=null,this._sendPong=!1,this._promises={},this._promiseId=0,this._debugEnabled=!1,this._config=Object.assign(Object.assign({},Q),t),this._configure(),this._debugEnabled?(this.on("state",(e=>{this._debug("client state",e.oldState,"->",e.newState)})),this.on("error",(e=>{this._debug("client error",e)}))):this.on("error",(function(){Function.prototype()}))}newSubscription(e,t){if(null!==this.getSubscription(e))throw new Error("Subscription to the channel "+e+" already exists");const s=new J.Subscription(this,e,t);return this._subs[e]=s,s}getSubscription(e){return this._getSub(e)}removeSubscription(e){e&&(e.state!==X.SubscriptionState.Unsubscribed&&e.unsubscribe(),this._removeSubscription(e))}subscriptions(){return this._subs}ready(e){return this.state===X.State.Disconnected?Promise.reject({code:z.errorCodes.clientDisconnected,message:"client disconnected"}):this.state===X.State.Connected?Promise.resolve():new Promise(((t,s)=>{const n={resolve:t,reject:s};e&&(n.timeout=setTimeout((function(){s({code:z.errorCodes.timeout,message:"timeout"})}),e)),this._promises[this._nextPromiseId()]=n}))}connect(){this._isConnected()?this._debug("connect called when already connected"):this._isConnecting()?this._debug("connect called when already connecting"):(this._reconnectAttempts=0,this._startConnecting())}disconnect(){this._disconnect(z.disconnectedCodes.disconnectCalled,"disconnect called",!1)}send(e){const t={send:{data:e}},s=this;return this._methodCall().then((function(){return s._transportSendCommands([t])?Promise.resolve():Promise.reject(s._createErrorObject(z.errorCodes.transportWriteError,"transport write error"))}))}rpc(e,t){const s={rpc:{method:e,data:t}},n=this;return this._methodCall().then((function(){return n._callPromise(s,(function(e){return{data:e.rpc.data}}))}))}publish(e,t){const s={publish:{channel:e,data:t}},n=this;return this._methodCall().then((function(){return n._callPromise(s,(function(){return{}}))}))}history(e,t){const s={history:this._getHistoryRequest(e,t)},n=this;return this._methodCall().then((function(){return n._callPromise(s,(function(t){const s=t.history,i=[];if(s.publications)for(let t=0;t=this._transports.length&&(this._triedAllTransports=!0,this._currentTransportIndex=0);let r=0;for(;;){if(r>=this._transports.length)throw new Error("no supported transport found");const o=this._transports[this._currentTransportIndex],c=o.transport,a=o.endpoint;if("websocket"===c){if(this._debug("trying websocket transport"),this._transport=new F.WebsocketTransport(a,{websocket:e}),!this._transport.supported()){this._debug("websocket transport not available"),this._currentTransportIndex++,r++;continue}}else if("webtransport"===c){if(this._debug("trying webtransport transport"),this._transport=new H.WebtransportTransport(a,{webtransport:globalThis.WebTransport,decoder:this._decoder,encoder:this._encoder}),!this._transport.supported()){this._debug("webtransport transport not available"),this._currentTransportIndex++,r++;continue}}else if("http_stream"===c){if(this._debug("trying http_stream transport"),this._transport=new q.HttpStreamTransport(a,{fetch:n,readableStream:i,emulationEndpoint:this._config.emulationEndpoint,decoder:this._decoder,encoder:this._encoder}),!this._transport.supported()){this._debug("http_stream transport not available"),this._currentTransportIndex++,r++;continue}}else if("sse"===c){if(this._debug("trying sse transport"),this._transport=new B.SseTransport(a,{eventsource:s,fetch:n,emulationEndpoint:this._config.emulationEndpoint}),!this._transport.supported()){this._debug("sse transport not available"),this._currentTransportIndex++,r++;continue}}else{if("sockjs"!==c)throw new Error("unknown transport "+c);if(this._debug("trying sockjs"),this._transport=new N.SockjsTransport(a,{sockjs:t,sockjsOptions:this._config.sockjsOptions}),!this._transport.supported()){this._debug("sockjs transport not available"),this._currentTransportIndex++,r++;continue}}break}}else{if((0,K.startsWith)(this._endpoint,"http"))throw new Error("Provide explicit transport endpoints configuration in case of using HTTP (i.e. using array of TransportEndpoint instead of a single string), or use ws(s):// scheme in an endpoint if you aimed using WebSocket transport");if(this._debug("client will use websocket"),this._transport=new F.WebsocketTransport(this._endpoint,{websocket:e}),!this._transport.supported())throw new Error("WebSocket not available")}const r=this;let o,c=!1,a=!0;"sse"===this._transport.name()&&(a=!1);const h=[];if(this._transport.emulation()){const e=r._sendConnect(!0);if(h.push(e),a){const e=r._sendSubscribeCommands(!0,!0);for(const t in e)h.push(e[t])}}const u=this._encoder.encodeCommands(h);this._transport.initialize(this._config.protocol,{onOpen:function(){c=!0,o=r._transport.subName(),r._debug(o,"transport open"),r._transportWasOpen=!0,r._transportClosed=!1,r._transport.emulation()||(r.startBatching(),r._sendConnect(!1),a&&r._sendSubscribeCommands(!0,!1),r.stopBatching())},onError:function(e){r._debug("transport level error",e)},onClose:function(e){r._debug(r._transport.name(),"transport closed"),r._transportClosed=!0;let t="connection closed",s=!0,n=0;if(e&&"code"in e&&e.code&&(n=e.code),e&&e.reason)try{const n=JSON.parse(e.reason);t=n.reason,s=n.reconnect}catch(i){t=e.reason,(n>=3500&&n<4e3||n>=4500&&n<5e3)&&(s=!1)}n<3e3?(1009===n?(n=z.disconnectedCodes.messageSizeLimit,t="message size limit exceeded",s=!1):(n=z.connectingCodes.transportClosed,t="transport closed"),r._emulation&&!r._transportWasOpen&&(r._currentTransportIndex++,r._currentTransportIndex>=r._transports.length&&(r._triedAllTransports=!0,r._currentTransportIndex=0))):r._transportWasOpen=!0;let i=!1;if(!r._emulation||r._transportWasOpen||r._triedAllTransports||(i=!0),r._isConnecting()&&!c&&r.emit("error",{type:"transport",error:{code:z.errorCodes.transportClosed,message:"transport closed"},transport:r._transport.name()}),r._disconnect(n,t,s),r._isConnecting()){let e=r._getReconnectDelay();i&&(e=0),r._debug("reconnect after "+e+" milliseconds"),r._reconnectTimeout=setTimeout((()=>{r._startReconnecting()}),e)}},onMessage:function(e){r._dataReceived(e)}},u)}_sendConnect(e){const t=this._constructConnectCommand(),s=this;return this._call(t,e).then((e=>{const t=e.reply.connect;s._connectResponse(t),e.next&&e.next()}),(e=>{s._connectError(e.error),e.next&&e.next()})),t}_startReconnecting(){if(!this._isConnecting())return;if(!(this._refreshRequired||!this._token&&null!==this._config.getToken))return void this._initializeTransport();const e=this;this._getToken().then((function(t){e._isConnecting()&&(t?(e._token=t,e._debug("connection token refreshed"),e._initializeTransport()):e._failUnauthorized())})).catch((function(t){if(!e._isConnecting())return;e.emit("error",{type:"connectToken",error:{code:z.errorCodes.clientConnectToken,message:void 0!==t?t.toString():""}});const s=e._getReconnectDelay();e._debug("error on connection token refresh, reconnect after "+s+" milliseconds",t),e._reconnectTimeout=setTimeout((()=>{e._startReconnecting()}),s)}))}_connectError(e){this.state===X.State.Connecting&&(109===e.code&&(this._refreshRequired=!0),e.code<100||!0===e.temporary||109===e.code?(this.emit("error",{type:"connect",error:e}),this._transport&&!this._transportClosed&&(this._transportClosed=!0,this._transport.close())):this._disconnect(e.code,e.message,!1))}_constructConnectCommand(){const e={};this._token&&(e.token=this._token),this._config.data&&(e.data=this._config.data),this._config.name&&(e.name=this._config.name),this._config.version&&(e.version=this._config.version);const t={};let s=!1;for(const e in this._serverSubs)if(this._serverSubs.hasOwnProperty(e)&&this._serverSubs[e].recoverable){s=!0;const n={recover:!0};this._serverSubs[e].offset&&(n.offset=this._serverSubs[e].offset),this._serverSubs[e].epoch&&(n.epoch=this._serverSubs[e].epoch),t[e]=n}return s&&(e.subs=t),{connect:e}}_getHistoryRequest(e,t){const s={channel:e};return void 0!==t&&(t.since&&(s.since={offset:t.since.offset},t.since.epoch&&(s.since.epoch=t.since.epoch)),void 0!==t.limit&&(s.limit=t.limit),!0===t.reverse&&(s.reverse=!0)),s}_methodCall(){return this._isConnected()?Promise.resolve():new Promise(((e,t)=>{const s=setTimeout((function(){t({code:z.errorCodes.timeout,message:"timeout"})}),this._config.timeout);this._promises[this._nextPromiseId()]={timeout:s,resolve:e,reject:t}}))}_callPromise(e,t){return new Promise(((s,n)=>{this._call(e,!1).then((e=>{s(t(e.reply)),e.next&&e.next()}),(e=>{n(e.error),e.next&&e.next()}))}))}_dataReceived(e){this._serverPing>0&&this._waitServerPing();const t=this._decoder.decodeReplies(e);this._dispatchPromise=this._dispatchPromise.then((()=>{let e;this._dispatchPromise=new Promise((t=>{e=t})),this._dispatchSynchronized(t,e)}))}_dispatchSynchronized(e,t){let s=Promise.resolve();for(const t in e)e.hasOwnProperty(t)&&(s=s.then((()=>this._dispatchReply(e[t]))));s=s.then((()=>{t()}))}_dispatchReply(e){let t;const s=new Promise((e=>{t=e}));if(null==e)return this._debug("dispatch: got undefined or null reply"),t(),s;const n=e.id;return n&&n>0?this._handleReply(e,t):e.push?this._handlePush(e.push,t):this._handleServerPing(t),s}_call(e,t){return new Promise(((s,n)=>{e.id=this._nextCommandId(),this._registerCall(e.id,s,n),t||this._addCommand(e)}))}_startConnecting(){this._debug("start connecting"),this._setState(X.State.Connecting)&&this.emit("connecting",{code:z.connectingCodes.connectCalled,reason:"connect called"}),this._client=null,this._startReconnecting()}_disconnect(e,t,s){if(this._isDisconnected())return;const n=this.state,i={code:e,reason:t};let r=!1;s?r=this._setState(X.State.Connecting):(r=this._setState(X.State.Disconnected),this._rejectPromises({code:z.errorCodes.clientDisconnected,message:"disconnected"})),this._clearOutgoingRequests(),n===X.State.Connecting&&this._clearReconnectTimeout(),n===X.State.Connected&&this._clearConnectedState(),r&&(this._isConnecting()?this.emit("connecting",i):this.emit("disconnected",i)),this._transport&&!this._transportClosed&&(this._transportClosed=!0,this._transport.close())}_failUnauthorized(){this._disconnect(z.disconnectedCodes.unauthorized,"unauthorized",!1)}_getToken(){if(this._debug("get connection token"),!this._config.getToken)throw new Error("provide a function to get connection token");return this._config.getToken({})}_refresh(){const e=this._client,t=this;this._getToken().then((function(s){if(e!==t._client)return;if(!s)return void t._failUnauthorized();if(t._token=s,t._debug("connection token refreshed"),!t._isConnected())return;const n={refresh:{token:t._token}};t._call(n,!1).then((e=>{const s=e.reply.refresh;t._refreshResponse(s),e.next&&e.next()}),(e=>{t._refreshError(e.error),e.next&&e.next()}))})).catch((function(e){t.emit("error",{type:"refreshToken",error:{code:z.errorCodes.clientRefreshToken,message:void 0!==e?e.toString():""}}),t._refreshTimeout=setTimeout((()=>t._refresh()),t._getRefreshRetryDelay())}))}_refreshError(e){e.code<100||!0===e.temporary?(this.emit("error",{type:"refresh",error:e}),this._refreshTimeout=setTimeout((()=>this._refresh()),this._getRefreshRetryDelay())):this._disconnect(e.code,e.message,!1)}_getRefreshRetryDelay(){return(0,K.backoff)(0,5e3,1e4)}_refreshResponse(e){this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null),e.expires&&(this._client=e.client,this._refreshTimeout=setTimeout((()=>this._refresh()),(0,K.ttlMilliseconds)(e.ttl)))}_removeSubscription(e){null!==e&&delete this._subs[e.channel]}_unsubscribe(e){if(!this._isConnected())return;const t={unsubscribe:{channel:e.channel}},s=this;this._call(t,!1).then((e=>{e.next&&e.next()}),(e=>{e.next&&e.next(),s._disconnect(z.connectingCodes.unsubscribeError,"unsubscribe error",!0)}))}_getSub(e){const t=this._subs[e];return t||null}_isServerSub(e){return void 0!==this._serverSubs[e]}_sendSubscribeCommands(e,t){const s=[];for(const n in this._subs){if(!this._subs.hasOwnProperty(n))continue;const i=this._subs[n];if(!0!==i._inflight&&i.state===X.SubscriptionState.Subscribing){const n=i._subscribe(e,t);n&&s.push(n)}}return s}_connectResponse(e){if(this._transportWasOpen=!0,this._reconnectAttempts=0,this._refreshRequired=!1,this._isConnected())return;this._client=e.client,this._setState(X.State.Connected),this._refreshTimeout&&clearTimeout(this._refreshTimeout),e.expires&&(this._refreshTimeout=setTimeout((()=>this._refresh()),(0,K.ttlMilliseconds)(e.ttl))),this._session=e.session,this._node=e.node,this.startBatching(),this._sendSubscribeCommands(!1,!1),this.stopBatching();const t={client:e.client,transport:this._transport.subName()};e.data&&(t.data=e.data),this.emit("connected",t),this._resolvePromises(),this._processServerSubs(e.subs||{}),e.ping&&e.ping>0?(this._serverPing=1e3*e.ping,this._sendPong=!0===e.pong,this._waitServerPing()):this._serverPing=0}_processServerSubs(e){for(const t in e){if(!e.hasOwnProperty(t))continue;const s=e[t];this._serverSubs[t]={offset:s.offset,epoch:s.epoch,recoverable:s.recoverable||!1};const n=this._getSubscribeContext(t,s);this.emit("subscribed",n)}for(const t in e){if(!e.hasOwnProperty(t))continue;const s=e[t];if(s.recovered){const e=s.publications;if(e&&e.length>0)for(const s in e)e.hasOwnProperty(s)&&this._handlePublication(t,e[s])}}for(const t in this._serverSubs)this._serverSubs.hasOwnProperty(t)&&(e[t]||(this.emit("unsubscribed",{channel:t}),delete this._serverSubs[t]))}_clearRefreshTimeout(){null!==this._refreshTimeout&&(clearTimeout(this._refreshTimeout),this._refreshTimeout=null)}_clearReconnectTimeout(){null!==this._reconnectTimeout&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null)}_clearServerPingTimeout(){null!==this._serverPingTimeout&&(clearTimeout(this._serverPingTimeout),this._serverPingTimeout=null)}_waitServerPing(){0!==this._config.maxServerPingDelay&&this._isConnected()&&(this._clearServerPingTimeout(),this._serverPingTimeout=setTimeout((()=>{this._isConnected()&&this._disconnect(z.connectingCodes.noPing,"no ping",!0)}),this._serverPing+this._config.maxServerPingDelay))}_getSubscribeContext(e,t){const s={channel:e,positioned:!1,recoverable:!1,wasRecovering:!1,recovered:!1};t.recovered&&(s.recovered=!0),t.positioned&&(s.positioned=!0),t.recoverable&&(s.recoverable=!0),t.was_recovering&&(s.wasRecovering=!0);let n="";"epoch"in t&&(n=t.epoch);let i=0;return"offset"in t&&(i=t.offset),(s.positioned||s.recoverable)&&(s.streamPosition={offset:i,epoch:n}),t.data&&(s.data=t.data),s}_handleReply(e,t){const s=e.id;if(!(s in this._callbacks))return void t();const n=this._callbacks[s];if(clearTimeout(this._callbacks[s].timeout),delete this._callbacks[s],(0,K.errorExists)(e)){const s=n.errback;if(!s)return void t();s({error:e.error,next:t})}else{const s=n.callback;if(!s)return;s({reply:e,next:t})}}_handleJoin(e,t){const s=this._getSub(e);if(s)s._handleJoin(t);else if(this._isServerSub(e)){const s={channel:e,info:this._getJoinLeaveContext(t.info)};this.emit("join",s)}}_handleLeave(e,t){const s=this._getSub(e);if(s)s._handleLeave(t);else if(this._isServerSub(e)){const s={channel:e,info:this._getJoinLeaveContext(t.info)};this.emit("leave",s)}}_handleUnsubscribe(e,t){const s=this._getSub(e);s?t.code<2500?s._setUnsubscribed(t.code,t.reason,!1):s._setSubscribing(t.code,t.reason):this._isServerSub(e)&&(delete this._serverSubs[e],this.emit("unsubscribed",{channel:e}))}_handleSubscribe(e,t){this._serverSubs[e]={offset:t.offset,epoch:t.epoch,recoverable:t.recoverable||!1},this.emit("subscribed",this._getSubscribeContext(e,t))}_handleDisconnect(e){const t=e.code;let s=!0;(t>=3500&&t<4e3||t>=4500&&t<5e3)&&(s=!1),this._disconnect(t,e.reason,s)}_getPublicationContext(e,t){const s={channel:e,data:t.data};return t.offset&&(s.offset=t.offset),t.info&&(s.info=this._getJoinLeaveContext(t.info)),t.tags&&(s.tags=t.tags),s}_getJoinLeaveContext(e){const t={client:e.client,user:e.user};return e.conn_info&&(t.connInfo=e.conn_info),e.chan_info&&(t.chanInfo=e.chan_info),t}_handlePublication(e,t){const s=this._getSub(e);if(s)s._handlePublication(t);else if(this._isServerSub(e)){const s=this._getPublicationContext(e,t);this.emit("publication",s),void 0!==t.offset&&(this._serverSubs[e].offset=t.offset)}}_handleMessage(e){this.emit("message",{data:e.data})}_handleServerPing(e){if(this._sendPong){const e={};this._transportSendCommands([e])}e()}_handlePush(e,t){const s=e.channel;e.pub?this._handlePublication(s,e.pub):e.message?this._handleMessage(e.message):e.join?this._handleJoin(s,e.join):e.leave?this._handleLeave(s,e.leave):e.unsubscribe?this._handleUnsubscribe(s,e.unsubscribe):e.subscribe?this._handleSubscribe(s,e.subscribe):e.disconnect&&this._handleDisconnect(e.disconnect),t()}_flush(){const e=this._commands.slice(0);this._commands=[],this._transportSendCommands(e)}_createErrorObject(e,t,s){const n={code:e,message:t};return s&&(n.temporary=!0),n}_registerCall(e,t,s){this._callbacks[e]={callback:t,errback:s,timeout:null},this._callbacks[e].timeout=setTimeout((()=>{delete this._callbacks[e],(0,K.isFunction)(s)&&s({error:this._createErrorObject(z.errorCodes.timeout,"timeout")})}),this._config.timeout)}_addCommand(e){this._batching?this._commands.push(e):this._transportSendCommands([e])}_nextPromiseId(){return++this._promiseId}_resolvePromises(){for(const e in this._promises)this._promises[e].timeout&&clearTimeout(this._promises[e].timeout),this._promises[e].resolve(),delete this._promises[e]}_rejectPromises(e){for(const t in this._promises)this._promises[t].timeout&&clearTimeout(this._promises[t].timeout),this._promises[t].reject(e),delete this._promises[t]}}i.Centrifuge=V,V.SubscriptionState=X.SubscriptionState,V.State=X.State,function(e){var t=s&&s.__createBinding||(Object.create?function(e,t,s,n){void 0===n&&(n=s);var i=Object.getOwnPropertyDescriptor(t,s);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,n,i)}:function(e,t,s,n){void 0===n&&(n=s),e[n]=t[s]}),n=s&&s.__exportStar||function(e,s){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(s,n)||t(s,e,n)};Object.defineProperty(e,"__esModule",{value:!0}),e.Subscription=e.Centrifuge=void 0;const o=i;Object.defineProperty(e,"Centrifuge",{enumerable:!0,get:function(){return o.Centrifuge}});const c=r;Object.defineProperty(e,"Subscription",{enumerable:!0,get:function(){return c.Subscription}}),n(T,e)}(n);class Y{channelName;msg;data;eventClass;site;bucket;date;constructor(e){this.channelName=e.channel,this.site=e.site,this.msg=void 0===e.message?"":e.message,this.data=void 0===e.data?{}:e.data,this.eventClass=void 0===e.event_class?"":e.event_class,this.bucket=void 0===e.bucket?"":e.bucket,this.date=new Date}}return e.Message=Y,e.useInstant=()=>{let e,t,s,i="";const r=new Set;let o,c=e=>console.log(JSON.stringify(e,null," "));const a=new Promise((e=>{o=e})),h=async()=>{const e=l({}),s=t+"/instant/get_token/",n=await fetch(s,e);if(!n.ok)throw console.log("Response not ok",n),new Error(n.statusText);const r=await n.json();return d(r),i=r.csrf_token,r.ws_token},u=()=>{r.forEach((t=>{console.log("Subscribing to",t);const s=e.newSubscription(t.name,{token:t.token});s.on("error",(function(e){console.log("subscription error",e)})),s.on("publication",(function(e){const t=_(JSON.parse(e.data));c(t)})),s.subscribe()}))},l=e=>{const t={method:"post",credentials:"include",mode:"cors",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}};return""!==i&&(t.headers={"Content-Type":"application/json","X-CSRFToken":i}),t},d=e=>{console.log("Tokens",JSON.stringify(e,null," ")),i=e.csrf_token,e.channels.forEach((e=>{r.add(e)}))},_=e=>{let t;try{t=new Y(e)}catch(t){throw new Error(`Can not process message ${e} ${t}`)}return t};return{init:async(i,r,c=!1)=>{s=r,t=i;const a=await h();e=new n.Centrifuge(`${s}/connection/websocket`,{token:a,debug:c}),o(!0)},connect:async(t=!0)=>{let s;const n=new Promise((e=>{s=e}));e.on("connected",(function(){s(!0)})),e.connect(),await n,t&&u()},onMessage:e=>{c=e},login:async(e,s)=>{console.log("Login");const n=l({username:e,password:s}),i=t+"/instant/login/",r=await fetch(i,n);if(!r.ok)throw console.log("Response not ok",r),new Error(r.statusText)},subscribe:()=>{u()},getClient:()=>e,onReady:a,channels:r}},Object.defineProperty(e,"__esModule",{value:!0}),e}({});
2 |
--------------------------------------------------------------------------------