django-cfg 1.4.62__py3-none-any.whl → 1.4.63__py3-none-any.whl
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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/services/otp_service.py +3 -14
- django_cfg/apps/centrifugo/__init__.py +57 -0
- django_cfg/apps/centrifugo/admin/__init__.py +13 -0
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +249 -0
- django_cfg/apps/centrifugo/admin/config.py +82 -0
- django_cfg/apps/centrifugo/apps.py +31 -0
- django_cfg/apps/centrifugo/codegen/IMPLEMENTATION_SUMMARY.md +475 -0
- django_cfg/apps/centrifugo/codegen/README.md +242 -0
- django_cfg/apps/centrifugo/codegen/USAGE.md +616 -0
- django_cfg/apps/centrifugo/codegen/__init__.py +19 -0
- django_cfg/apps/centrifugo/codegen/discovery.py +246 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/generator.py +174 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/README.md.j2 +182 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/client.go.j2 +64 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/go.mod.j2 +10 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2 +300 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/rpc_client.go.j2.old +267 -0
- django_cfg/apps/centrifugo/codegen/generators/go_thin/templates/types.go.j2 +16 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/__init__.py +7 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/generator.py +241 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/README.md.j2 +128 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/__init__.py.j2 +22 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/client.py.j2 +73 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/models.py.j2 +19 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/requirements.txt.j2 +8 -0
- django_cfg/apps/centrifugo/codegen/generators/python_thin/templates/rpc_client.py.j2 +193 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/__init__.py +5 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/generator.py +124 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/README.md.j2 +38 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/client.ts.j2 +25 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/index.ts.j2 +12 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/package.json.j2 +13 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +137 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/tsconfig.json.j2 +14 -0
- django_cfg/apps/centrifugo/codegen/generators/typescript_thin/templates/types.ts.j2 +9 -0
- django_cfg/apps/centrifugo/codegen/utils/__init__.py +37 -0
- django_cfg/apps/centrifugo/codegen/utils/naming.py +155 -0
- django_cfg/apps/centrifugo/codegen/utils/type_converter.py +349 -0
- django_cfg/apps/centrifugo/decorators.py +137 -0
- django_cfg/apps/centrifugo/management/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/__init__.py +1 -0
- django_cfg/apps/centrifugo/management/commands/generate_centrifugo_clients.py +254 -0
- django_cfg/apps/centrifugo/managers/__init__.py +12 -0
- django_cfg/apps/centrifugo/managers/centrifugo_log.py +264 -0
- django_cfg/apps/centrifugo/migrations/0001_initial.py +164 -0
- django_cfg/apps/centrifugo/migrations/__init__.py +3 -0
- django_cfg/apps/centrifugo/models/__init__.py +11 -0
- django_cfg/apps/centrifugo/models/centrifugo_log.py +210 -0
- django_cfg/apps/centrifugo/registry.py +106 -0
- django_cfg/apps/centrifugo/router.py +125 -0
- django_cfg/apps/centrifugo/serializers/__init__.py +40 -0
- django_cfg/apps/centrifugo/serializers/admin_api.py +264 -0
- django_cfg/apps/centrifugo/serializers/channels.py +26 -0
- django_cfg/apps/centrifugo/serializers/health.py +17 -0
- django_cfg/apps/centrifugo/serializers/publishes.py +16 -0
- django_cfg/apps/centrifugo/serializers/stats.py +21 -0
- django_cfg/apps/centrifugo/services/__init__.py +12 -0
- django_cfg/apps/centrifugo/services/client/__init__.py +29 -0
- django_cfg/apps/centrifugo/services/client/client.py +577 -0
- django_cfg/apps/centrifugo/services/client/config.py +228 -0
- django_cfg/apps/centrifugo/services/client/exceptions.py +212 -0
- django_cfg/apps/centrifugo/services/config_helper.py +63 -0
- django_cfg/apps/centrifugo/services/dashboard_notifier.py +157 -0
- django_cfg/apps/centrifugo/services/logging.py +677 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +260 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +313 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +803 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +333 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +432 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +33 -0
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +210 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +46 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +123 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +45 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +84 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/stat_cards.html +23 -20
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +91 -0
- django_cfg/apps/{ipc/templates/django_cfg_ipc → centrifugo/templates/django_cfg_centrifugo}/components/tab_navigation.html +15 -15
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +415 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +61 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +58 -0
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +48 -0
- django_cfg/apps/centrifugo/templatetags/__init__.py +1 -0
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +81 -0
- django_cfg/apps/centrifugo/urls.py +31 -0
- django_cfg/apps/{ipc → centrifugo}/urls_admin.py +4 -4
- django_cfg/apps/centrifugo/views/__init__.py +15 -0
- django_cfg/apps/centrifugo/views/admin_api.py +374 -0
- django_cfg/apps/centrifugo/views/dashboard.py +15 -0
- django_cfg/apps/centrifugo/views/monitoring.py +286 -0
- django_cfg/apps/centrifugo/views/testing_api.py +422 -0
- django_cfg/apps/support/utils/support_email_service.py +5 -18
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -2
- django_cfg/apps/urls.py +5 -5
- django_cfg/core/base/config_model.py +4 -44
- django_cfg/core/builders/apps_builder.py +2 -2
- django_cfg/core/generation/integration_generators/third_party.py +8 -8
- django_cfg/core/utils/__init__.py +5 -0
- django_cfg/core/utils/url_helpers.py +73 -0
- django_cfg/modules/base.py +7 -7
- django_cfg/modules/django_client/core/__init__.py +2 -1
- django_cfg/modules/django_client/core/config/config.py +8 -0
- django_cfg/modules/django_client/core/generator/__init__.py +42 -2
- django_cfg/modules/django_client/core/generator/go/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/go/client_generator.py +124 -0
- django_cfg/modules/django_client/core/generator/go/files_generator.py +133 -0
- django_cfg/modules/django_client/core/generator/go/generator.py +203 -0
- django_cfg/modules/django_client/core/generator/go/models_generator.py +304 -0
- django_cfg/modules/django_client/core/generator/go/naming.py +193 -0
- django_cfg/modules/django_client/core/generator/go/operations_generator.py +134 -0
- django_cfg/modules/django_client/core/generator/go/templates/Makefile.j2 +38 -0
- django_cfg/modules/django_client/core/generator/go/templates/README.md.j2 +55 -0
- django_cfg/modules/django_client/core/generator/go/templates/client.go.j2 +122 -0
- django_cfg/modules/django_client/core/generator/go/templates/enums.go.j2 +49 -0
- django_cfg/modules/django_client/core/generator/go/templates/errors.go.j2 +182 -0
- django_cfg/modules/django_client/core/generator/go/templates/go.mod.j2 +6 -0
- django_cfg/modules/django_client/core/generator/go/templates/main_client.go.j2 +60 -0
- django_cfg/modules/django_client/core/generator/go/templates/middleware.go.j2 +388 -0
- django_cfg/modules/django_client/core/generator/go/templates/models.go.j2 +28 -0
- django_cfg/modules/django_client/core/generator/go/templates/operations_client.go.j2 +142 -0
- django_cfg/modules/django_client/core/generator/go/templates/validation.go.j2 +217 -0
- django_cfg/modules/django_client/core/generator/go/type_mapper.py +380 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +53 -3
- django_cfg/modules/django_client/system/generate_mjs_clients.py +3 -1
- django_cfg/modules/django_client/system/schema_parser.py +5 -1
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +1 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +7 -4
- django_cfg/modules/django_unfold/dashboard.py +25 -19
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/core.py +2 -0
- django_cfg/registry/modules.py +2 -2
- django_cfg/static/js/api/centrifugo/client.mjs +164 -0
- django_cfg/static/js/api/centrifugo/index.mjs +13 -0
- django_cfg/static/js/api/index.mjs +5 -5
- django_cfg/static/js/api/types.mjs +89 -26
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/METADATA +1 -1
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/RECORD +142 -70
- django_cfg/apps/ipc/README.md +0 -346
- django_cfg/apps/ipc/RPC_LOGGING.md +0 -321
- django_cfg/apps/ipc/TESTING.md +0 -539
- django_cfg/apps/ipc/__init__.py +0 -60
- django_cfg/apps/ipc/admin.py +0 -232
- django_cfg/apps/ipc/apps.py +0 -98
- django_cfg/apps/ipc/migrations/0001_initial.py +0 -137
- django_cfg/apps/ipc/migrations/0002_rpclog_is_event.py +0 -23
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +0 -229
- django_cfg/apps/ipc/serializers/__init__.py +0 -29
- django_cfg/apps/ipc/serializers/serializers.py +0 -343
- django_cfg/apps/ipc/services/__init__.py +0 -7
- django_cfg/apps/ipc/services/client/__init__.py +0 -23
- django_cfg/apps/ipc/services/client/client.py +0 -621
- django_cfg/apps/ipc/services/client/config.py +0 -214
- django_cfg/apps/ipc/services/client/exceptions.py +0 -201
- django_cfg/apps/ipc/services/logging.py +0 -239
- django_cfg/apps/ipc/services/monitor.py +0 -466
- django_cfg/apps/ipc/services/rpc_log_consumer.py +0 -330
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +0 -269
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +0 -259
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +0 -375
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard.mjs.old +0 -441
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +0 -22
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +0 -9
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +0 -23
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +0 -47
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +0 -184
- django_cfg/apps/ipc/templates/django_cfg_ipc/layout/base.html +0 -71
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +0 -56
- django_cfg/apps/ipc/urls.py +0 -23
- django_cfg/apps/ipc/views/__init__.py +0 -13
- django_cfg/apps/ipc/views/dashboard.py +0 -15
- django_cfg/apps/ipc/views/monitoring.py +0 -251
- django_cfg/apps/ipc/views/testing.py +0 -285
- django_cfg/static/js/api/ipc/client.mjs +0 -114
- django_cfg/static/js/api/ipc/index.mjs +0 -13
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.62.dist-info → django_cfg-1.4.63.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
package {{ package_name }}
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"crypto/rand"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"sync"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"nhooyr.io/websocket"
|
|
15
|
+
"nhooyr.io/websocket/wsjson"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// generateUUID creates a random UUID v4 using crypto/rand (stdlib only)
|
|
19
|
+
func generateUUID() string {
|
|
20
|
+
b := make([]byte, 16)
|
|
21
|
+
if _, err := rand.Read(b); err != nil {
|
|
22
|
+
// Fallback to timestamp if crypto fails
|
|
23
|
+
return fmt.Sprintf("%d", time.Now().UnixNano())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Set version (4) and variant (RFC4122)
|
|
27
|
+
b[6] = (b[6] & 0x0f) | 0x40
|
|
28
|
+
b[8] = (b[8] & 0x3f) | 0x80
|
|
29
|
+
|
|
30
|
+
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// CentrifugoRPCClient handles WebSocket connection and RPC call correlation.
|
|
34
|
+
type CentrifugoRPCClient struct {
|
|
35
|
+
url string
|
|
36
|
+
token string
|
|
37
|
+
userID string
|
|
38
|
+
timeout time.Duration
|
|
39
|
+
conn *websocket.Conn
|
|
40
|
+
mu sync.RWMutex
|
|
41
|
+
pending map[string]chan RPCResponse
|
|
42
|
+
replyChannel string
|
|
43
|
+
ctx context.Context
|
|
44
|
+
cancel context.CancelFunc
|
|
45
|
+
closeChan chan struct{}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// RPCRequest represents an RPC request message.
|
|
49
|
+
type RPCRequest struct {
|
|
50
|
+
Method string `json:"method"`
|
|
51
|
+
Params interface{} `json:"params"`
|
|
52
|
+
CorrelationID string `json:"correlation_id"`
|
|
53
|
+
ReplyTo string `json:"reply_to"`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// RPCResponse represents an RPC response message.
|
|
57
|
+
type RPCResponse struct {
|
|
58
|
+
CorrelationID string `json:"correlation_id"`
|
|
59
|
+
Result json.RawMessage `json:"result,omitempty"`
|
|
60
|
+
Error *RPCError `json:"error,omitempty"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// RPCError represents an RPC error.
|
|
64
|
+
type RPCError struct {
|
|
65
|
+
Code int `json:"code"`
|
|
66
|
+
Message string `json:"message"`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func (e *RPCError) Error() string {
|
|
70
|
+
return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// NewCentrifugoRPCClient creates a new Centrifugo RPC client.
|
|
74
|
+
//
|
|
75
|
+
// Args:
|
|
76
|
+
// - url: WebSocket URL (e.g., "ws://localhost:8000/connection/websocket")
|
|
77
|
+
// - token: JWT authentication token
|
|
78
|
+
// - userID: User ID for reply channel
|
|
79
|
+
// - timeout: Default timeout for RPC calls
|
|
80
|
+
func NewCentrifugoRPCClient(url, token, userID string, timeout time.Duration) *CentrifugoRPCClient {
|
|
81
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
82
|
+
return &CentrifugoRPCClient{
|
|
83
|
+
url: url,
|
|
84
|
+
token: token,
|
|
85
|
+
userID: userID,
|
|
86
|
+
timeout: timeout,
|
|
87
|
+
pending: make(map[string]chan RPCResponse),
|
|
88
|
+
replyChannel: fmt.Sprintf("user#%s", userID),
|
|
89
|
+
ctx: ctx,
|
|
90
|
+
cancel: cancel,
|
|
91
|
+
closeChan: make(chan struct{}),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Connect establishes WebSocket connection to Centrifugo.
|
|
96
|
+
func (c *CentrifugoRPCClient) Connect(ctx context.Context) error {
|
|
97
|
+
c.mu.Lock()
|
|
98
|
+
defer c.mu.Unlock()
|
|
99
|
+
|
|
100
|
+
if c.conn != nil {
|
|
101
|
+
return errors.New("already connected")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Connect to WebSocket
|
|
105
|
+
conn, _, err := websocket.Dial(ctx, c.url, &websocket.DialOptions{
|
|
106
|
+
Subprotocols: []string{"centrifuge-protobuf"},
|
|
107
|
+
})
|
|
108
|
+
if err != nil {
|
|
109
|
+
return fmt.Errorf("failed to dial: %w", err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
c.conn = conn
|
|
113
|
+
|
|
114
|
+
// Start message reader goroutine
|
|
115
|
+
go c.readLoop()
|
|
116
|
+
|
|
117
|
+
// Subscribe to reply channel
|
|
118
|
+
if err := c.subscribe(ctx, c.replyChannel); err != nil {
|
|
119
|
+
c.conn.Close(websocket.StatusNormalClosure, "")
|
|
120
|
+
c.conn = nil
|
|
121
|
+
return fmt.Errorf("failed to subscribe: %w", err)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// subscribe subscribes to a Centrifugo channel
|
|
128
|
+
func (c *CentrifugoRPCClient) subscribe(ctx context.Context, channel string) error {
|
|
129
|
+
subscribeMsg := map[string]interface{}{
|
|
130
|
+
"subscribe": map[string]interface{}{
|
|
131
|
+
"channel": channel,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return wsjson.Write(ctx, c.conn, subscribeMsg)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// readLoop reads messages from WebSocket connection
|
|
139
|
+
func (c *CentrifugoRPCClient) readLoop() {
|
|
140
|
+
defer func() {
|
|
141
|
+
c.mu.Lock()
|
|
142
|
+
close(c.closeChan)
|
|
143
|
+
c.mu.Unlock()
|
|
144
|
+
}()
|
|
145
|
+
|
|
146
|
+
for {
|
|
147
|
+
select {
|
|
148
|
+
case <-c.ctx.Done():
|
|
149
|
+
return
|
|
150
|
+
default:
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var msg map[string]interface{}
|
|
154
|
+
err := wsjson.Read(c.ctx, c.conn, &msg)
|
|
155
|
+
if err != nil {
|
|
156
|
+
if websocket.CloseStatus(err) != websocket.StatusNormalClosure {
|
|
157
|
+
fmt.Printf("WebSocket read error: %v\n", err)
|
|
158
|
+
}
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle publication messages (responses to our RPC calls)
|
|
163
|
+
if pub, ok := msg["pub"].(map[string]interface{}); ok {
|
|
164
|
+
if data, ok := pub["data"].(map[string]interface{}); ok {
|
|
165
|
+
c.handleResponse(data)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// handleResponse processes incoming RPC responses
|
|
172
|
+
func (c *CentrifugoRPCClient) handleResponse(data map[string]interface{}) {
|
|
173
|
+
correlationID, ok := data["correlation_id"].(string)
|
|
174
|
+
if !ok {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
c.mu.RLock()
|
|
179
|
+
responseChan, exists := c.pending[correlationID]
|
|
180
|
+
c.mu.RUnlock()
|
|
181
|
+
|
|
182
|
+
if !exists {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Parse response
|
|
187
|
+
var response RPCResponse
|
|
188
|
+
response.CorrelationID = correlationID
|
|
189
|
+
|
|
190
|
+
if result, ok := data["result"]; ok {
|
|
191
|
+
resultBytes, _ := json.Marshal(result)
|
|
192
|
+
response.Result = resultBytes
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if errData, ok := data["error"].(map[string]interface{}); ok {
|
|
196
|
+
rpcErr := &RPCError{}
|
|
197
|
+
if code, ok := errData["code"].(float64); ok {
|
|
198
|
+
rpcErr.Code = int(code)
|
|
199
|
+
}
|
|
200
|
+
if msg, ok := errData["message"].(string); ok {
|
|
201
|
+
rpcErr.Message = msg
|
|
202
|
+
}
|
|
203
|
+
response.Error = rpcErr
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Send response to waiting goroutine
|
|
207
|
+
select {
|
|
208
|
+
case responseChan <- response:
|
|
209
|
+
case <-time.After(time.Second):
|
|
210
|
+
fmt.Printf("Timeout sending response for correlation_id: %s\n", correlationID)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Call performs an RPC call with automatic serialization/deserialization.
|
|
215
|
+
//
|
|
216
|
+
// Args:
|
|
217
|
+
// - ctx: Context for timeout and cancellation
|
|
218
|
+
// - method: RPC method name (e.g., "tasks.get_stats")
|
|
219
|
+
// - params: Request parameters (will be JSON-serialized)
|
|
220
|
+
// - result: Pointer to result struct (will be JSON-deserialized)
|
|
221
|
+
//
|
|
222
|
+
// Returns error if call fails or times out.
|
|
223
|
+
func (c *CentrifugoRPCClient) Call(ctx context.Context, method string, params interface{}, result interface{}) error {
|
|
224
|
+
correlationID := generateUUID()
|
|
225
|
+
|
|
226
|
+
// Create request
|
|
227
|
+
request := RPCRequest{
|
|
228
|
+
Method: method,
|
|
229
|
+
Params: params,
|
|
230
|
+
CorrelationID: correlationID,
|
|
231
|
+
ReplyTo: c.replyChannel,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Create response channel
|
|
235
|
+
responseChan := make(chan RPCResponse, 1)
|
|
236
|
+
c.mu.Lock()
|
|
237
|
+
c.pending[correlationID] = responseChan
|
|
238
|
+
c.mu.Unlock()
|
|
239
|
+
|
|
240
|
+
defer func() {
|
|
241
|
+
c.mu.Lock()
|
|
242
|
+
delete(c.pending, correlationID)
|
|
243
|
+
c.mu.Unlock()
|
|
244
|
+
close(responseChan)
|
|
245
|
+
}()
|
|
246
|
+
|
|
247
|
+
// Publish request to rpc.requests channel
|
|
248
|
+
publishMsg := map[string]interface{}{
|
|
249
|
+
"publish": map[string]interface{}{
|
|
250
|
+
"channel": "rpc.requests",
|
|
251
|
+
"data": request,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if err := wsjson.Write(ctx, c.conn, publishMsg); err != nil {
|
|
256
|
+
return fmt.Errorf("failed to publish request: %w", err)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Wait for response with timeout
|
|
260
|
+
timeout := c.timeout
|
|
261
|
+
if deadline, ok := ctx.Deadline(); ok {
|
|
262
|
+
timeout = time.Until(deadline)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
select {
|
|
266
|
+
case response := <-responseChan:
|
|
267
|
+
if response.Error != nil {
|
|
268
|
+
return response.Error
|
|
269
|
+
}
|
|
270
|
+
if result != nil && len(response.Result) > 0 {
|
|
271
|
+
return json.Unmarshal(response.Result, result)
|
|
272
|
+
}
|
|
273
|
+
return nil
|
|
274
|
+
|
|
275
|
+
case <-time.After(timeout):
|
|
276
|
+
return fmt.Errorf("RPC call timed out after %v", timeout)
|
|
277
|
+
|
|
278
|
+
case <-ctx.Done():
|
|
279
|
+
return ctx.Err()
|
|
280
|
+
|
|
281
|
+
case <-c.closeChan:
|
|
282
|
+
return errors.New("connection closed")
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Disconnect closes the WebSocket connection.
|
|
287
|
+
func (c *CentrifugoRPCClient) Disconnect() error {
|
|
288
|
+
c.cancel()
|
|
289
|
+
|
|
290
|
+
c.mu.Lock()
|
|
291
|
+
defer c.mu.Unlock()
|
|
292
|
+
|
|
293
|
+
if c.conn == nil {
|
|
294
|
+
return nil
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
err := c.conn.Close(websocket.StatusNormalClosure, "client disconnect")
|
|
298
|
+
c.conn = nil
|
|
299
|
+
return err
|
|
300
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
package {{ package_name }}
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"crypto/rand"
|
|
8
|
+
"encoding/hex"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"fmt"
|
|
11
|
+
"sync"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"nhooyr.io/websocket"
|
|
15
|
+
"nhooyr.io/websocket/wsjson"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// generateUUID creates a random UUID v4 using crypto/rand
|
|
19
|
+
func generateUUID() (string, error) {
|
|
20
|
+
uuid := make([]byte, 16)
|
|
21
|
+
if _, err := rand.Read(uuid); err != nil {
|
|
22
|
+
return "", err
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Set version (4) and variant bits
|
|
26
|
+
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
|
27
|
+
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
|
28
|
+
|
|
29
|
+
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// CentrifugoRPCClient handles WebSocket connection and RPC call correlation.
|
|
33
|
+
type CentrifugoRPCClient struct {
|
|
34
|
+
url string
|
|
35
|
+
token string
|
|
36
|
+
userID string
|
|
37
|
+
timeout time.Duration
|
|
38
|
+
conn *websocket.Conn
|
|
39
|
+
pendingMutex sync.RWMutex
|
|
40
|
+
pendingRequests map[string]chan RPCResponse
|
|
41
|
+
replyChannel string
|
|
42
|
+
closeChan chan struct{}
|
|
43
|
+
readMutex sync.Mutex
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// RPCRequest represents an RPC request message.
|
|
47
|
+
type RPCRequest struct {
|
|
48
|
+
Method string `json:"method"`
|
|
49
|
+
Params interface{} `json:"params"`
|
|
50
|
+
CorrelationID string `json:"correlation_id"`
|
|
51
|
+
ReplyTo string `json:"reply_to"`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// RPCResponse represents an RPC response message.
|
|
55
|
+
type RPCResponse struct {
|
|
56
|
+
CorrelationID string `json:"correlation_id"`
|
|
57
|
+
Result json.RawMessage `json:"result,omitempty"`
|
|
58
|
+
Error *RPCError `json:"error,omitempty"`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// RPCError represents an RPC error.
|
|
62
|
+
type RPCError struct {
|
|
63
|
+
Code int `json:"code"`
|
|
64
|
+
Message string `json:"message"`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func (e *RPCError) Error() string {
|
|
68
|
+
return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// NewCentrifugoRPCClient creates a new RPC client.
|
|
72
|
+
//
|
|
73
|
+
// Args:
|
|
74
|
+
// - url: Centrifugo WebSocket URL (e.g., "ws://localhost:8000/connection/websocket")
|
|
75
|
+
// - token: JWT token for authentication
|
|
76
|
+
// - userID: User ID for reply channel
|
|
77
|
+
// - timeout: RPC call timeout
|
|
78
|
+
func NewCentrifugoRPCClient(url, token, userID string, timeout time.Duration) *CentrifugoRPCClient {
|
|
79
|
+
return &CentrifugoRPCClient{
|
|
80
|
+
url: url,
|
|
81
|
+
token: token,
|
|
82
|
+
userID: userID,
|
|
83
|
+
timeout: timeout,
|
|
84
|
+
pendingRequests: make(map[string]chan RPCResponse),
|
|
85
|
+
replyChannel: fmt.Sprintf("user#%s", userID),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Connect establishes WebSocket connection to Centrifugo.
|
|
90
|
+
func (c *CentrifugoRPCClient) Connect(ctx context.Context) error {
|
|
91
|
+
client := centrifuge.NewJsonClient(
|
|
92
|
+
c.url,
|
|
93
|
+
centrifuge.Config{
|
|
94
|
+
Token: c.token,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
client.OnConnected(func(e centrifuge.ConnectedEvent) {
|
|
99
|
+
fmt.Printf("✅ Connected to Centrifugo: client_id=%s\n", e.ClientID)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
client.OnDisconnected(func(e centrifuge.DisconnectedEvent) {
|
|
103
|
+
fmt.Printf("⚠️ Disconnected from Centrifugo: code=%d, reason=%s\n", e.Code, e.Reason)
|
|
104
|
+
c.rejectAllPending(fmt.Errorf("disconnected from Centrifugo"))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
c.client = client
|
|
108
|
+
|
|
109
|
+
// Connect to server
|
|
110
|
+
if err := client.Connect(); err != nil {
|
|
111
|
+
return fmt.Errorf("failed to connect: %w", err)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Subscribe to reply channel
|
|
115
|
+
sub, err := client.NewSubscription(c.replyChannel)
|
|
116
|
+
if err != nil {
|
|
117
|
+
return fmt.Errorf("failed to create subscription: %w", err)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
sub.OnPublication(func(e centrifuge.PublicationEvent) {
|
|
121
|
+
c.handleResponse(e.Data)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if err := sub.Subscribe(); err != nil {
|
|
125
|
+
return fmt.Errorf("failed to subscribe to reply channel: %w", err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Disconnect closes the WebSocket connection.
|
|
132
|
+
func (c *CentrifugoRPCClient) Disconnect() error {
|
|
133
|
+
if c.client != nil {
|
|
134
|
+
if err := c.client.Disconnect(); err != nil {
|
|
135
|
+
return err
|
|
136
|
+
}
|
|
137
|
+
c.client = nil
|
|
138
|
+
}
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Call makes an RPC call and waits for the response.
|
|
143
|
+
//
|
|
144
|
+
// Args:
|
|
145
|
+
// - ctx: Context for cancellation
|
|
146
|
+
// - method: RPC method name (e.g., "tasks.get_stats")
|
|
147
|
+
// - params: Method parameters
|
|
148
|
+
// - result: Pointer to result struct
|
|
149
|
+
//
|
|
150
|
+
// Returns error if RPC call fails or times out.
|
|
151
|
+
func (c *CentrifugoRPCClient) Call(ctx context.Context, method string, params interface{}, result interface{}) error {
|
|
152
|
+
if c.client == nil {
|
|
153
|
+
return fmt.Errorf("not connected to Centrifugo")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Generate correlation ID
|
|
157
|
+
correlationID := uuid.New().String()
|
|
158
|
+
|
|
159
|
+
// Create request
|
|
160
|
+
request := RPCRequest{
|
|
161
|
+
Method: method,
|
|
162
|
+
Params: params,
|
|
163
|
+
CorrelationID: correlationID,
|
|
164
|
+
ReplyTo: c.replyChannel,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
requestData, err := json.Marshal(request)
|
|
168
|
+
if err != nil {
|
|
169
|
+
return fmt.Errorf("failed to marshal request: %w", err)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create response channel
|
|
173
|
+
responseChan := make(chan RPCResponse, 1)
|
|
174
|
+
c.pendingMutex.Lock()
|
|
175
|
+
c.pendingRequests[correlationID] = responseChan
|
|
176
|
+
c.pendingMutex.Unlock()
|
|
177
|
+
|
|
178
|
+
// Publish request
|
|
179
|
+
_, err = c.client.Publish(ctx, "rpc.requests", requestData)
|
|
180
|
+
if err != nil {
|
|
181
|
+
c.pendingMutex.Lock()
|
|
182
|
+
delete(c.pendingRequests, correlationID)
|
|
183
|
+
c.pendingMutex.Unlock()
|
|
184
|
+
return fmt.Errorf("failed to publish request: %w", err)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fmt.Printf("📤 RPC call: %s (correlation_id: %s)\n", method, correlationID)
|
|
188
|
+
|
|
189
|
+
// Wait for response with timeout
|
|
190
|
+
select {
|
|
191
|
+
case response := <-responseChan:
|
|
192
|
+
fmt.Printf("📥 RPC response: %s (correlation_id: %s)\n", method, correlationID)
|
|
193
|
+
|
|
194
|
+
if response.Error != nil {
|
|
195
|
+
return response.Error
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if result != nil {
|
|
199
|
+
if err := json.Unmarshal(response.Result, result); err != nil {
|
|
200
|
+
return fmt.Errorf("failed to unmarshal result: %w", err)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return nil
|
|
205
|
+
|
|
206
|
+
case <-time.After(c.timeout):
|
|
207
|
+
c.pendingMutex.Lock()
|
|
208
|
+
delete(c.pendingRequests, correlationID)
|
|
209
|
+
c.pendingMutex.Unlock()
|
|
210
|
+
return fmt.Errorf("RPC timeout: %s (correlation_id: %s)", method, correlationID)
|
|
211
|
+
|
|
212
|
+
case <-ctx.Done():
|
|
213
|
+
c.pendingMutex.Lock()
|
|
214
|
+
delete(c.pendingRequests, correlationID)
|
|
215
|
+
c.pendingMutex.Unlock()
|
|
216
|
+
return ctx.Err()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// handleResponse processes incoming RPC responses.
|
|
221
|
+
func (c *CentrifugoRPCClient) handleResponse(data []byte) {
|
|
222
|
+
var response RPCResponse
|
|
223
|
+
if err := json.Unmarshal(data, &response); err != nil {
|
|
224
|
+
fmt.Printf("⚠️ Failed to unmarshal response: %v\n", err)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
correlationID := response.CorrelationID
|
|
229
|
+
if correlationID == "" {
|
|
230
|
+
fmt.Println("⚠️ Received response without correlation_id")
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
c.pendingMutex.RLock()
|
|
235
|
+
responseChan, exists := c.pendingRequests[correlationID]
|
|
236
|
+
c.pendingMutex.RUnlock()
|
|
237
|
+
|
|
238
|
+
if !exists {
|
|
239
|
+
fmt.Printf("⚠️ Received response for unknown correlation_id: %s\n", correlationID)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Send response to waiting goroutine
|
|
244
|
+
responseChan <- response
|
|
245
|
+
|
|
246
|
+
// Clean up
|
|
247
|
+
c.pendingMutex.Lock()
|
|
248
|
+
delete(c.pendingRequests, correlationID)
|
|
249
|
+
c.pendingMutex.Unlock()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// rejectAllPending rejects all pending requests with an error.
|
|
253
|
+
func (c *CentrifugoRPCClient) rejectAllPending(err error) {
|
|
254
|
+
c.pendingMutex.Lock()
|
|
255
|
+
defer c.pendingMutex.Unlock()
|
|
256
|
+
|
|
257
|
+
for correlationID, responseChan := range c.pendingRequests {
|
|
258
|
+
responseChan <- RPCResponse{
|
|
259
|
+
CorrelationID: correlationID,
|
|
260
|
+
Error: &RPCError{
|
|
261
|
+
Code: -1,
|
|
262
|
+
Message: err.Error(),
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
delete(c.pendingRequests, correlationID)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Code generated by django-cfg centrifugo codegen. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
package {{ package_name }}
|
|
4
|
+
|
|
5
|
+
{% for type in types %}
|
|
6
|
+
// {{ type.doc }}
|
|
7
|
+
type {{ type.name }} struct {
|
|
8
|
+
{% for field in type.fields %}
|
|
9
|
+
{% if field.description %}
|
|
10
|
+
// {{ field.description }}
|
|
11
|
+
{% endif %}
|
|
12
|
+
{{ field.name }} {{ field.type }} {{ field.json_tag }}
|
|
13
|
+
{% endfor %}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
{% endfor %}
|