pywire 0.1.1__py3-none-any.whl → 0.1.2__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.
Files changed (101) hide show
  1. pywire/__init__.py +2 -0
  2. pywire/cli/__init__.py +1 -0
  3. pywire/cli/generators.py +48 -0
  4. pywire/cli/main.py +309 -0
  5. pywire/cli/tui.py +563 -0
  6. pywire/cli/validate.py +26 -0
  7. pywire/client/.prettierignore +8 -0
  8. pywire/client/.prettierrc +7 -0
  9. pywire/client/build.mjs +73 -0
  10. pywire/client/eslint.config.js +46 -0
  11. pywire/client/package.json +39 -0
  12. pywire/client/pnpm-lock.yaml +2971 -0
  13. pywire/client/src/core/app.ts +263 -0
  14. pywire/client/src/core/dom-updater.test.ts +78 -0
  15. pywire/client/src/core/dom-updater.ts +321 -0
  16. pywire/client/src/core/index.ts +5 -0
  17. pywire/client/src/core/transport-manager.test.ts +179 -0
  18. pywire/client/src/core/transport-manager.ts +159 -0
  19. pywire/client/src/core/transports/base.ts +122 -0
  20. pywire/client/src/core/transports/http.ts +142 -0
  21. pywire/client/src/core/transports/index.ts +13 -0
  22. pywire/client/src/core/transports/websocket.ts +97 -0
  23. pywire/client/src/core/transports/webtransport.ts +149 -0
  24. pywire/client/src/dev/dev-app.ts +93 -0
  25. pywire/client/src/dev/error-trace.test.ts +97 -0
  26. pywire/client/src/dev/error-trace.ts +76 -0
  27. pywire/client/src/dev/index.ts +4 -0
  28. pywire/client/src/dev/status-overlay.ts +63 -0
  29. pywire/client/src/events/handler.test.ts +318 -0
  30. pywire/client/src/events/handler.ts +454 -0
  31. pywire/client/src/pywire.core.ts +22 -0
  32. pywire/client/src/pywire.dev.ts +27 -0
  33. pywire/client/tsconfig.json +17 -0
  34. pywire/client/vitest.config.ts +15 -0
  35. pywire/compiler/__init__.py +6 -0
  36. pywire/compiler/ast_nodes.py +304 -0
  37. pywire/compiler/attributes/__init__.py +6 -0
  38. pywire/compiler/attributes/base.py +24 -0
  39. pywire/compiler/attributes/conditional.py +37 -0
  40. pywire/compiler/attributes/events.py +55 -0
  41. pywire/compiler/attributes/form.py +37 -0
  42. pywire/compiler/attributes/loop.py +75 -0
  43. pywire/compiler/attributes/reactive.py +34 -0
  44. pywire/compiler/build.py +28 -0
  45. pywire/compiler/build_artifacts.py +342 -0
  46. pywire/compiler/codegen/__init__.py +5 -0
  47. pywire/compiler/codegen/attributes/__init__.py +6 -0
  48. pywire/compiler/codegen/attributes/base.py +19 -0
  49. pywire/compiler/codegen/attributes/events.py +35 -0
  50. pywire/compiler/codegen/directives/__init__.py +6 -0
  51. pywire/compiler/codegen/directives/base.py +16 -0
  52. pywire/compiler/codegen/directives/path.py +53 -0
  53. pywire/compiler/codegen/generator.py +2341 -0
  54. pywire/compiler/codegen/template.py +2178 -0
  55. pywire/compiler/directives/__init__.py +7 -0
  56. pywire/compiler/directives/base.py +20 -0
  57. pywire/compiler/directives/component.py +33 -0
  58. pywire/compiler/directives/context.py +93 -0
  59. pywire/compiler/directives/layout.py +49 -0
  60. pywire/compiler/directives/no_spa.py +24 -0
  61. pywire/compiler/directives/path.py +71 -0
  62. pywire/compiler/directives/props.py +88 -0
  63. pywire/compiler/exceptions.py +19 -0
  64. pywire/compiler/interpolation/__init__.py +6 -0
  65. pywire/compiler/interpolation/base.py +28 -0
  66. pywire/compiler/interpolation/jinja.py +272 -0
  67. pywire/compiler/parser.py +750 -0
  68. pywire/compiler/paths.py +29 -0
  69. pywire/compiler/preprocessor.py +43 -0
  70. pywire/core/wire.py +119 -0
  71. pywire/py.typed +0 -0
  72. pywire/runtime/__init__.py +7 -0
  73. pywire/runtime/aioquic_server.py +194 -0
  74. pywire/runtime/app.py +901 -0
  75. pywire/runtime/compile_error_page.py +195 -0
  76. pywire/runtime/debug.py +203 -0
  77. pywire/runtime/dev_server.py +434 -0
  78. pywire/runtime/dev_server.py.broken +268 -0
  79. pywire/runtime/error_page.py +64 -0
  80. pywire/runtime/error_renderer.py +23 -0
  81. pywire/runtime/escape.py +23 -0
  82. pywire/runtime/files.py +40 -0
  83. pywire/runtime/helpers.py +97 -0
  84. pywire/runtime/http_transport.py +253 -0
  85. pywire/runtime/loader.py +272 -0
  86. pywire/runtime/logging.py +72 -0
  87. pywire/runtime/page.py +384 -0
  88. pywire/runtime/pydantic_integration.py +52 -0
  89. pywire/runtime/router.py +229 -0
  90. pywire/runtime/server.py +25 -0
  91. pywire/runtime/style_collector.py +31 -0
  92. pywire/runtime/upload_manager.py +76 -0
  93. pywire/runtime/validation.py +449 -0
  94. pywire/runtime/websocket.py +665 -0
  95. pywire/runtime/webtransport_handler.py +195 -0
  96. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/METADATA +1 -1
  97. pywire-0.1.2.dist-info/RECORD +104 -0
  98. pywire-0.1.1.dist-info/RECORD +0 -9
  99. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/WHEEL +0 -0
  100. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/entry_points.txt +0 -0
  101. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { TransportManager } from './transport-manager'
3
+ import { WebTransportTransport } from './transports/webtransport'
4
+ import { WebSocketTransport } from './transports/websocket'
5
+ import { HTTPTransport } from './transports/http'
6
+
7
+ // Create mock classes that can be used in the tests
8
+ class MockWebTransport {
9
+ static isSupported = vi.fn(() => true)
10
+ connect = vi.fn().mockResolvedValue(undefined)
11
+ onMessage = vi.fn()
12
+ onStatusChange = vi.fn()
13
+ isConnected = vi.fn(() => false)
14
+ send = vi.fn()
15
+ disconnect = vi.fn()
16
+ name = 'WebTransportTransport'
17
+ }
18
+
19
+ class MockWebSocket {
20
+ connect = vi.fn().mockResolvedValue(undefined)
21
+ onMessage = vi.fn()
22
+ onStatusChange = vi.fn()
23
+ isConnected = vi.fn(() => false)
24
+ send = vi.fn()
25
+ disconnect = vi.fn()
26
+ name = 'WebSocketTransport'
27
+ }
28
+
29
+ class MockHTTP {
30
+ connect = vi.fn().mockResolvedValue(undefined)
31
+ onMessage = vi.fn()
32
+ onStatusChange = vi.fn()
33
+ isConnected = vi.fn(() => false)
34
+ send = vi.fn()
35
+ disconnect = vi.fn()
36
+ name = 'HTTPTransport'
37
+ }
38
+
39
+ vi.mock('./transports/webtransport', () => ({
40
+ WebTransportTransport: vi.fn(),
41
+ }))
42
+
43
+ vi.mock('./transports/websocket', () => ({
44
+ WebSocketTransport: vi.fn(),
45
+ }))
46
+
47
+ vi.mock('./transports/http', () => ({
48
+ HTTPTransport: vi.fn(),
49
+ }))
50
+
51
+ type MockedTransportCtor = {
52
+ mockImplementation: (impl: new (...args: unknown[]) => unknown) => void
53
+ }
54
+
55
+ type MockedWebTransportCtor = MockedTransportCtor & {
56
+ isSupported: ReturnType<typeof vi.fn>
57
+ }
58
+
59
+ const WebTransportMock = WebTransportTransport as unknown as MockedWebTransportCtor
60
+ const WebSocketMock = WebSocketTransport as unknown as MockedTransportCtor
61
+ const HTTPMock = HTTPTransport as unknown as MockedTransportCtor
62
+
63
+ // Set static properties on the mocked constructors
64
+ WebTransportMock.isSupported = MockWebTransport.isSupported
65
+ Object.defineProperty(WebTransportTransport, 'name', { value: 'WebTransportTransport' })
66
+ Object.defineProperty(WebSocketTransport, 'name', { value: 'WebSocketTransport' })
67
+ Object.defineProperty(HTTPTransport, 'name', { value: 'HTTPTransport' })
68
+
69
+ describe('TransportManager', () => {
70
+ beforeEach(() => {
71
+ vi.clearAllMocks()
72
+
73
+ // Default mock implementations (successful connect)
74
+ WebTransportMock.mockImplementation(MockWebTransport)
75
+ WebSocketMock.mockImplementation(MockWebSocket)
76
+ HTTPMock.mockImplementation(MockHTTP)
77
+
78
+ vi.stubGlobal('location', { protocol: 'https:' })
79
+ vi.stubGlobal('WebSocket', vi.fn())
80
+ WebTransportMock.isSupported.mockReturnValue(true)
81
+ })
82
+
83
+ it('should try WebTransport first if supported and on HTTPS', async () => {
84
+ const manager = new TransportManager()
85
+ await manager.connect()
86
+
87
+ expect(WebTransportTransport).toHaveBeenCalled()
88
+ expect(manager.getActiveTransport()).toBe('WebTransportTransport')
89
+ })
90
+
91
+ it('should fallback to WebSocket if WebTransport fails', async () => {
92
+ WebTransportMock.mockImplementation(
93
+ class extends MockWebTransport {
94
+ connect = vi.fn().mockRejectedValue(new Error('WT failed'))
95
+ }
96
+ )
97
+
98
+ const manager = new TransportManager()
99
+ await manager.connect()
100
+
101
+ expect(WebTransportTransport).toHaveBeenCalled()
102
+ expect(WebSocketTransport).toHaveBeenCalled()
103
+ expect(manager.getActiveTransport()).toBe('WebSocketTransport')
104
+ })
105
+
106
+ it('should fallback to HTTP if WebSocket fails', async () => {
107
+ WebTransportMock.mockImplementation(
108
+ class extends MockWebTransport {
109
+ connect = vi.fn().mockRejectedValue(new Error('WT failed'))
110
+ }
111
+ )
112
+ WebSocketMock.mockImplementation(
113
+ class extends MockWebSocket {
114
+ connect = vi.fn().mockRejectedValue(new Error('WS failed'))
115
+ }
116
+ )
117
+
118
+ const manager = new TransportManager()
119
+ await manager.connect()
120
+
121
+ expect(HTTPTransport).toHaveBeenCalled()
122
+ expect(manager.getActiveTransport()).toBe('HTTPTransport')
123
+ })
124
+
125
+ it('should skip WebTransport if not on HTTPS', async () => {
126
+ vi.stubGlobal('location', { protocol: 'http:' })
127
+ const manager = new TransportManager()
128
+ await manager.connect()
129
+
130
+ expect(WebTransportTransport).not.toHaveBeenCalled()
131
+ expect(WebSocketTransport).toHaveBeenCalled()
132
+ })
133
+
134
+ it('should respect config to disable transports', async () => {
135
+ const manager = new TransportManager({ enableWebTransport: false })
136
+ await manager.connect()
137
+
138
+ expect(WebTransportTransport).not.toHaveBeenCalled()
139
+ expect(WebSocketTransport).toHaveBeenCalled()
140
+ })
141
+
142
+ it('should throw if all transports fail', async () => {
143
+ WebTransportMock.mockImplementation(
144
+ class extends MockWebTransport {
145
+ connect = vi.fn().mockRejectedValue(new Error('WT failed'))
146
+ }
147
+ )
148
+ WebSocketMock.mockImplementation(
149
+ class extends MockWebSocket {
150
+ connect = vi.fn().mockRejectedValue(new Error('WS failed'))
151
+ }
152
+ )
153
+ HTTPMock.mockImplementation(
154
+ class extends MockHTTP {
155
+ connect = vi.fn().mockRejectedValue(new Error('HTTP failed'))
156
+ }
157
+ )
158
+
159
+ const manager = new TransportManager()
160
+ await expect(manager.connect()).rejects.toThrow('PyWire: All transports failed')
161
+ })
162
+
163
+ it('should forward messages to registered handlers', async () => {
164
+ const onMessageSpy = vi.fn()
165
+ WebTransportMock.mockImplementation(
166
+ class extends MockWebTransport {
167
+ onMessage = onMessageSpy
168
+ }
169
+ )
170
+
171
+ const manager = new TransportManager()
172
+ const handler = vi.fn()
173
+ manager.onMessage(handler)
174
+
175
+ await manager.connect()
176
+
177
+ expect(onMessageSpy).toHaveBeenCalledWith(handler)
178
+ })
179
+ })
@@ -0,0 +1,159 @@
1
+ import { Transport, ServerMessage } from './transports'
2
+ import { WebTransportTransport } from './transports/webtransport'
3
+ import { WebSocketTransport } from './transports/websocket'
4
+ import { HTTPTransport } from './transports/http'
5
+
6
+ export interface TransportConfig {
7
+ /** Enable WebTransport (requires HTTPS and HTTP/3 server) */
8
+ enableWebTransport?: boolean
9
+ /** Enable WebSocket */
10
+ enableWebSocket?: boolean
11
+ /** Enable HTTP polling fallback */
12
+ enableHTTP?: boolean
13
+ /** Custom WebTransport URL */
14
+ webTransportUrl?: string
15
+ /** Custom WebSocket URL */
16
+ webSocketUrl?: string
17
+ /** Custom HTTP base URL */
18
+ httpUrl?: string
19
+ }
20
+
21
+ const DEFAULT_CONFIG: TransportConfig = {
22
+ enableWebTransport: true,
23
+ enableWebSocket: true,
24
+ enableHTTP: true,
25
+ }
26
+
27
+ /**
28
+ * Manages transport selection with automatic fallback.
29
+ * Tries transports in order: WebTransport → WebSocket → HTTP
30
+ */
31
+ export class TransportManager {
32
+ private transport: Transport | null = null
33
+ private config: TransportConfig
34
+ private messageHandlers: ((msg: ServerMessage) => void)[] = []
35
+ private statusHandlers: ((connected: boolean) => void)[] = []
36
+
37
+ constructor(config: Partial<TransportConfig> = {}) {
38
+ this.config = { ...DEFAULT_CONFIG, ...config }
39
+ }
40
+
41
+ /**
42
+ * Connect using the best available transport with fallback.
43
+ */
44
+ async connect(): Promise<void> {
45
+ const transports = this.getTransportPriority()
46
+
47
+ for (const TransportClass of transports) {
48
+ try {
49
+ console.log(`PyWire: Trying ${TransportClass.name}...`)
50
+ this.transport = new TransportClass()
51
+
52
+ // Forward message handlers
53
+ for (const handler of this.messageHandlers) {
54
+ this.transport.onMessage(handler)
55
+ }
56
+
57
+ // Forward status changes
58
+ this.transport.onStatusChange((connected) => {
59
+ this.notifyStatusHandlers(connected)
60
+ })
61
+
62
+ await this.transport.connect()
63
+ console.log(`PyWire: Connected via ${this.transport.name}`)
64
+ return
65
+ } catch (e) {
66
+ console.warn(`PyWire: ${TransportClass.name} failed, trying next...`, e)
67
+ this.transport = null
68
+ }
69
+ }
70
+
71
+ throw new Error('PyWire: All transports failed')
72
+ }
73
+
74
+ /**
75
+ * Get transport classes in priority order based on config and browser support.
76
+ */
77
+ private getTransportPriority(): (new (...args: unknown[]) => Transport)[] {
78
+ const transports: (new (...args: unknown[]) => Transport)[] = []
79
+
80
+ // WebTransport - only if supported and enabled
81
+ if (this.config.enableWebTransport && WebTransportTransport.isSupported()) {
82
+ // Also check if we're on HTTPS (required for WebTransport)
83
+ if (window.location.protocol === 'https:') {
84
+ transports.push(WebTransportTransport as unknown as new (...args: unknown[]) => Transport)
85
+ }
86
+ }
87
+
88
+ // WebSocket
89
+ if (this.config.enableWebSocket && typeof WebSocket !== 'undefined') {
90
+ transports.push(WebSocketTransport as unknown as new (...args: unknown[]) => Transport)
91
+ }
92
+
93
+ // HTTP (always available as final fallback)
94
+ if (this.config.enableHTTP) {
95
+ transports.push(HTTPTransport as unknown as new (...args: unknown[]) => Transport)
96
+ }
97
+
98
+ return transports
99
+ }
100
+
101
+ /**
102
+ * Send a message through the active transport.
103
+ */
104
+ send(message: object): void {
105
+ if (this.transport) {
106
+ this.transport.send(message)
107
+ } else {
108
+ console.warn('PyWire: No active transport')
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Register a message handler.
114
+ */
115
+ onMessage(handler: (msg: ServerMessage) => void): void {
116
+ this.messageHandlers.push(handler)
117
+ if (this.transport) {
118
+ this.transport.onMessage(handler)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Register a connection status change handler.
124
+ */
125
+ onStatusChange(handler: (connected: boolean) => void): void {
126
+ this.statusHandlers.push(handler)
127
+ }
128
+
129
+ private notifyStatusHandlers(connected: boolean): void {
130
+ for (const handler of this.statusHandlers) {
131
+ handler(connected)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Disconnect the active transport.
137
+ */
138
+ disconnect(): void {
139
+ if (this.transport) {
140
+ this.transport.disconnect()
141
+ this.transport = null
142
+ this.notifyStatusHandlers(false)
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get the name of the active transport.
148
+ */
149
+ getActiveTransport(): string | null {
150
+ return this.transport?.name || null
151
+ }
152
+
153
+ /**
154
+ * Check if connected.
155
+ */
156
+ isConnected(): boolean {
157
+ return this.transport?.isConnected() || false
158
+ }
159
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Base transport interface for all transport implementations.
3
+ */
4
+ export interface Transport {
5
+ /** Connect to the server */
6
+ connect(): Promise<void>
7
+
8
+ /** Send a message to the server */
9
+ send(message: object): void
10
+
11
+ /** Register a message handler */
12
+ onMessage(handler: MessageHandler): void
13
+
14
+ /** Register a status change handler */
15
+ onStatusChange(handler: (connected: boolean) => void): void
16
+
17
+ /** Disconnect from the server */
18
+ disconnect(): void
19
+
20
+ /** Check if connected */
21
+ isConnected(): boolean
22
+
23
+ /** Transport name for debugging */
24
+ readonly name: string
25
+ }
26
+
27
+ export type MessageHandler = (message: ServerMessage) => void
28
+
29
+ export interface StackFrame {
30
+ filename: string
31
+ lineno: number
32
+ name: string
33
+ line: string
34
+ colno?: number // Python 3.11+ column start
35
+ end_colno?: number // Python 3.11+ column end
36
+ }
37
+
38
+ export interface ServerMessage {
39
+ type: 'update' | 'reload' | 'error' | 'console' | 'error_trace'
40
+ html?: string
41
+ regions?: Array<{ region: string; html: string }>
42
+ error?: string
43
+ level?: 'info' | 'warn' | 'error'
44
+ lines?: string[]
45
+ trace?: StackFrame[]
46
+ }
47
+
48
+ export interface ConsoleMessage {
49
+ type: 'console'
50
+ level: 'info' | 'warn' | 'error'
51
+ lines: string[]
52
+ }
53
+
54
+ export type ClientMessage = EventMessage | RelocateMessage
55
+
56
+ export interface EventMessage {
57
+ type: 'event'
58
+ handler: string
59
+ path: string
60
+ data: EventData
61
+ }
62
+
63
+ export interface RelocateMessage {
64
+ type: 'relocate'
65
+ path: string
66
+ }
67
+
68
+ export interface EventData {
69
+ type: string
70
+ id?: string
71
+ value?: string
72
+ args?: Record<string, unknown>
73
+ [key: string]: unknown
74
+ }
75
+
76
+ /**
77
+ * Abstract base class providing common transport functionality.
78
+ */
79
+ export abstract class BaseTransport implements Transport {
80
+ protected messageHandlers: MessageHandler[] = []
81
+ protected statusHandlers: ((connected: boolean) => void)[] = []
82
+ protected connected = false
83
+
84
+ abstract readonly name: string
85
+ abstract connect(): Promise<void>
86
+ abstract send(message: object): void
87
+ abstract disconnect(): void
88
+
89
+ onMessage(handler: MessageHandler): void {
90
+ this.messageHandlers.push(handler)
91
+ }
92
+
93
+ onStatusChange(handler: (connected: boolean) => void): void {
94
+ this.statusHandlers.push(handler)
95
+ }
96
+
97
+ isConnected(): boolean {
98
+ return this.connected
99
+ }
100
+
101
+ protected notifyHandlers(message: ServerMessage): void {
102
+ for (const handler of this.messageHandlers) {
103
+ try {
104
+ handler(message)
105
+ } catch (e) {
106
+ console.error('PyWire: Error in message handler', e)
107
+ }
108
+ }
109
+ }
110
+
111
+ protected notifyStatus(connected: boolean): void {
112
+ if (this.connected === connected) return
113
+ this.connected = connected
114
+ for (const handler of this.statusHandlers) {
115
+ try {
116
+ handler(connected)
117
+ } catch (e) {
118
+ console.error('PyWire: Error in status handler', e)
119
+ }
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,142 @@
1
+ import { BaseTransport, ServerMessage } from './base'
2
+ import { encode, decode } from '@msgpack/msgpack'
3
+
4
+ /**
5
+ * HTTP polling transport as a fallback when WebTransport and WebSocket are unavailable.
6
+ * Uses long-polling for receiving updates and POST for sending events.
7
+ */
8
+ export class HTTPTransport extends BaseTransport {
9
+ readonly name = 'HTTP'
10
+
11
+ private polling = false
12
+ private pollAbortController: AbortController | null = null
13
+ private readonly baseUrl: string
14
+ private sessionId: string | null = null
15
+
16
+ constructor(baseUrl?: string) {
17
+ super()
18
+ this.baseUrl = baseUrl || `${window.location.origin}/_pywire`
19
+ }
20
+
21
+ async connect(): Promise<void> {
22
+ try {
23
+ // Initialize session with the server
24
+ const response = await fetch(`${this.baseUrl}/session`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/x-msgpack',
28
+ Accept: 'application/x-msgpack',
29
+ },
30
+ body: encode({ path: window.location.pathname + window.location.search }),
31
+ })
32
+
33
+ if (!response.ok) {
34
+ throw new Error(`HTTP session init failed: ${response.status}`)
35
+ }
36
+
37
+ const buffer = await response.arrayBuffer()
38
+ const data = decode(buffer) as { sessionId: string }
39
+ this.sessionId = data.sessionId
40
+
41
+ console.log('PyWire: HTTP transport connected')
42
+ this.notifyStatus(true)
43
+
44
+ // Start polling for updates
45
+ this.startPolling()
46
+ } catch (e) {
47
+ console.error('PyWire: HTTP transport connection failed', e)
48
+ throw e
49
+ }
50
+ }
51
+
52
+ private async startPolling(): Promise<void> {
53
+ if (this.polling) return
54
+ this.polling = true
55
+
56
+ while (this.polling && this.connected) {
57
+ try {
58
+ this.pollAbortController = new AbortController()
59
+
60
+ const response = await fetch(`${this.baseUrl}/poll?session=${this.sessionId}`, {
61
+ method: 'GET',
62
+ signal: this.pollAbortController.signal,
63
+ headers: {
64
+ Accept: 'application/x-msgpack',
65
+ },
66
+ })
67
+
68
+ if (!response.ok) {
69
+ if (response.status === 404) {
70
+ // Session expired, try to reconnect
71
+ console.warn('PyWire: HTTP session expired, reconnecting...')
72
+ this.notifyStatus(false)
73
+ await this.connect()
74
+ return
75
+ }
76
+ throw new Error(`Poll failed: ${response.status}`)
77
+ }
78
+
79
+ const buffer = await response.arrayBuffer()
80
+ const messages = decode(buffer) as ServerMessage[]
81
+
82
+ for (const msg of messages) {
83
+ this.notifyHandlers(msg)
84
+ }
85
+ } catch (e) {
86
+ if (e instanceof Error && e.name === 'AbortError') {
87
+ // Polling was aborted intentionally
88
+ break
89
+ }
90
+ console.error('PyWire: HTTP poll error', e)
91
+ // Wait a bit before retrying
92
+ await this.sleep(1000)
93
+ }
94
+ }
95
+ }
96
+
97
+ async send(message: object): Promise<void> {
98
+ if (!this.connected || !this.sessionId) {
99
+ console.warn('PyWire: Cannot send message, HTTP transport not connected')
100
+ return
101
+ }
102
+
103
+ try {
104
+ const response = await fetch(`${this.baseUrl}/event`, {
105
+ method: 'POST',
106
+ headers: {
107
+ 'Content-Type': 'application/x-msgpack',
108
+ Accept: 'application/x-msgpack',
109
+ 'X-PyWire-Session': this.sessionId,
110
+ },
111
+ body: encode(message),
112
+ })
113
+
114
+ if (!response.ok) {
115
+ throw new Error(`Event send failed: ${response.status}`)
116
+ }
117
+
118
+ // The response contains the updated HTML
119
+ const buffer = await response.arrayBuffer()
120
+ const result = decode(buffer) as ServerMessage
121
+ this.notifyHandlers(result)
122
+ } catch (e) {
123
+ console.error('PyWire: HTTP send error', e)
124
+ }
125
+ }
126
+
127
+ disconnect(): void {
128
+ this.polling = false
129
+ this.notifyStatus(false)
130
+
131
+ if (this.pollAbortController) {
132
+ this.pollAbortController.abort()
133
+ this.pollAbortController = null
134
+ }
135
+
136
+ this.sessionId = null
137
+ }
138
+
139
+ private sleep(ms: number): Promise<void> {
140
+ return new Promise((resolve) => setTimeout(resolve, ms))
141
+ }
142
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ Transport,
3
+ MessageHandler,
4
+ ServerMessage,
5
+ ClientMessage,
6
+ EventMessage,
7
+ RelocateMessage,
8
+ EventData,
9
+ StackFrame,
10
+ } from './base'
11
+ export { WebSocketTransport } from './websocket'
12
+ export { WebTransportTransport } from './webtransport'
13
+ export { HTTPTransport } from './http'
@@ -0,0 +1,97 @@
1
+ import { BaseTransport, ServerMessage } from './base'
2
+ import { encode, decode } from '@msgpack/msgpack'
3
+
4
+ /**
5
+ * WebSocket transport implementation.
6
+ */
7
+ export class WebSocketTransport extends BaseTransport {
8
+ readonly name = 'WebSocket'
9
+
10
+ private socket: WebSocket | null = null
11
+ private reconnectAttempts = 0
12
+ private maxReconnectDelay = 5000
13
+ private shouldReconnect = true
14
+ private readonly url: string
15
+
16
+ constructor(url?: string) {
17
+ super()
18
+ this.url = url || this.getDefaultUrl()
19
+ }
20
+
21
+ private getDefaultUrl(): string {
22
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
23
+ return `${protocol}//${window.location.host}/_pywire/ws`
24
+ }
25
+
26
+ connect(): Promise<void> {
27
+ return new Promise((resolve, reject) => {
28
+ try {
29
+ this.socket = new WebSocket(this.url)
30
+ this.socket.binaryType = 'arraybuffer'
31
+
32
+ this.socket.onopen = () => {
33
+ console.log('PyWire: WebSocket connected')
34
+ this.notifyStatus(true)
35
+ this.reconnectAttempts = 0
36
+ resolve()
37
+ }
38
+
39
+ this.socket.onmessage = (event: MessageEvent) => {
40
+ try {
41
+ const msg = decode(event.data) as ServerMessage
42
+ this.notifyHandlers(msg)
43
+ } catch (e) {
44
+ console.error('PyWire: Error parsing WebSocket message', e)
45
+ }
46
+ }
47
+
48
+ this.socket.onclose = () => {
49
+ console.log('PyWire: WebSocket disconnected')
50
+ this.notifyStatus(false)
51
+ if (this.shouldReconnect) {
52
+ this.scheduleReconnect()
53
+ }
54
+ }
55
+
56
+ this.socket.onerror = (error) => {
57
+ console.error('PyWire: WebSocket error', error)
58
+ if (!this.connected) {
59
+ reject(new Error('WebSocket connection failed'))
60
+ }
61
+ }
62
+ } catch (e) {
63
+ reject(e)
64
+ }
65
+ })
66
+ }
67
+
68
+ send(message: object): void {
69
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
70
+ this.socket.send(encode(message))
71
+ } else {
72
+ console.warn('PyWire: Cannot send message, WebSocket not open')
73
+ }
74
+ }
75
+
76
+ disconnect(): void {
77
+ this.shouldReconnect = false
78
+ if (this.socket) {
79
+ this.socket.close()
80
+ this.socket = null
81
+ }
82
+ this.notifyStatus(false)
83
+ }
84
+
85
+ private scheduleReconnect(): void {
86
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay)
87
+
88
+ console.log(`PyWire: Reconnecting in ${delay}ms...`)
89
+
90
+ setTimeout(() => {
91
+ this.reconnectAttempts++
92
+ this.connect().catch(() => {
93
+ // Reconnect will be scheduled again on close
94
+ })
95
+ }, delay)
96
+ }
97
+ }