thatcher 1.0.47 → 1.0.49
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.
- package/README.md +15 -15
- package/package.json +1 -1
- package/src/index.js +3 -0
- package/src/lib/automation-engine.js +113 -0
- package/src/lib/busybase/store.js +9 -5
- package/src/lib/crud-handlers.js +3 -3
- package/src/lib/field-types.js +45 -0
- package/src/server/server.js +42 -8
- package/src/services/permission.service.js +2 -2
- package/src/ui/board-view-renderer.js +76 -0
- package/src/ui/calendar-view-renderer.js +196 -0
- package/src/ui/grid-view-renderer.js +78 -0
- package/src/lib/compression.js +0 -44
- package/src/lib/route-resolver.js +0 -160
- package/src/lib/state-protocol.js +0 -184
- package/src/lib/state-transport-client.js +0 -178
- package/src/lib/state-transport-reconnect.js +0 -126
- package/src/lib/state-transport-server.js +0 -189
- package/src/lib/static-server.js +0 -97
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// state-sync channel (client side): browser WebSocket client with
|
|
2
|
-
// exponential-backoff reconnect + polling fallback, part of the
|
|
3
|
-
// state-protocol.js quartet. See the header comment in state-protocol.js --
|
|
4
|
-
// this is a structured, reconnect-aware protocol distinct from
|
|
5
|
-
// realtime-server.js's simple in-process pub/sub. Zero real importers found
|
|
6
|
-
// repo-wide as of this writing; currently dormant/unwired.
|
|
7
|
-
import Protocol from './state-protocol.js'
|
|
8
|
-
import { CONFIG, ReconnectManager } from './state-transport-reconnect.js'
|
|
9
|
-
import { createLogger } from './logger.js'
|
|
10
|
-
|
|
11
|
-
const log = createLogger('[StateTransport]')
|
|
12
|
-
|
|
13
|
-
class StateTransportClient {
|
|
14
|
-
constructor(config = {}) {
|
|
15
|
-
this.config = { ...CONFIG, ...config }
|
|
16
|
-
this.ws = null
|
|
17
|
-
this.state = 'disconnected'
|
|
18
|
-
this.listeners = new Map()
|
|
19
|
-
this.messageQueue = []
|
|
20
|
-
this.connectionTimeoutTimer = null
|
|
21
|
-
|
|
22
|
-
this.reconnect = new ReconnectManager(this.config, {
|
|
23
|
-
onReconnecting: (data) => { this.emit('reconnecting', data); this.connect() },
|
|
24
|
-
onFallback: (msg) => this.emit('fallback', msg),
|
|
25
|
-
onPollStart: () => this.emit('poll_start'),
|
|
26
|
-
onPollSuccess: (result) => this.emit('poll_success', result),
|
|
27
|
-
onPollError: (error) => this.emit('poll_error', error)
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
connect(url) {
|
|
32
|
-
this.url = url || this.url
|
|
33
|
-
if (!this.url) {
|
|
34
|
-
this.emit('error', new Error('No URL provided for connection'))
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
this.reconnect.wsUrl = this.url
|
|
38
|
-
|
|
39
|
-
if (this.state === 'connecting' || this.state === 'connected') return
|
|
40
|
-
|
|
41
|
-
this.state = 'connecting'
|
|
42
|
-
this.emit('connecting')
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
this.ws = new WebSocket(this.url)
|
|
46
|
-
|
|
47
|
-
this.connectionTimeoutTimer = setTimeout(() => {
|
|
48
|
-
if (this.state === 'connecting') this.handleConnectionTimeout()
|
|
49
|
-
}, this.config.connectionTimeout)
|
|
50
|
-
|
|
51
|
-
this.ws.onopen = () => this.handleOpen()
|
|
52
|
-
this.ws.onclose = (event) => this.handleClose(event)
|
|
53
|
-
this.ws.onerror = (error) => this.handleError(error)
|
|
54
|
-
this.ws.onmessage = (event) => this.handleMessage(event)
|
|
55
|
-
} catch (error) {
|
|
56
|
-
this.handleConnectionFailure(error)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
handleOpen() {
|
|
61
|
-
clearTimeout(this.connectionTimeoutTimer)
|
|
62
|
-
this.state = 'connected'
|
|
63
|
-
this.reconnect.resetAttempts()
|
|
64
|
-
this.emit('connected')
|
|
65
|
-
this.reconnect.startPingInterval(
|
|
66
|
-
(msg) => this.send(msg),
|
|
67
|
-
() => this.ws.close()
|
|
68
|
-
)
|
|
69
|
-
this.flushMessageQueue()
|
|
70
|
-
this.reconnect.stopPolling()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
handleClose(event) {
|
|
74
|
-
clearTimeout(this.connectionTimeoutTimer)
|
|
75
|
-
this.state = 'disconnected'
|
|
76
|
-
this.reconnect.stopPingInterval()
|
|
77
|
-
this.emit('disconnected', { code: event.code, reason: event.reason })
|
|
78
|
-
this.reconnect.scheduleReconnect()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
handleError(error) {
|
|
82
|
-
this.emit('error', error)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
handleConnectionTimeout() {
|
|
86
|
-
if (this.ws) this.ws.close()
|
|
87
|
-
this.handleConnectionFailure(new Error('Connection timeout'))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
handleConnectionFailure(error) {
|
|
91
|
-
this.emit('error', error)
|
|
92
|
-
this.state = 'disconnected'
|
|
93
|
-
|
|
94
|
-
if (this.reconnect.shouldFallback) {
|
|
95
|
-
this.reconnect.activateFallback()
|
|
96
|
-
} else {
|
|
97
|
-
this.reconnect.scheduleReconnect()
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
handleMessage(event) {
|
|
102
|
-
try {
|
|
103
|
-
const message = JSON.parse(event.data)
|
|
104
|
-
if (message.type === Protocol.MESSAGE_TYPES.PONG) {
|
|
105
|
-
this.reconnect.recordPong()
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
this.emit('message', message)
|
|
109
|
-
} catch (error) {
|
|
110
|
-
this.emit('error', { error, phase: 'message_parsing', data: event.data })
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
send(message) {
|
|
115
|
-
if (this.state === 'connected' && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
116
|
-
try {
|
|
117
|
-
this.ws.send(JSON.stringify(message))
|
|
118
|
-
return true
|
|
119
|
-
} catch (error) {
|
|
120
|
-
this.emit('error', { error, phase: 'send' })
|
|
121
|
-
this.messageQueue.push(message)
|
|
122
|
-
return false
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
this.messageQueue.push(message)
|
|
126
|
-
return false
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
flushMessageQueue() {
|
|
130
|
-
while (this.messageQueue.length > 0 && this.state === 'connected') {
|
|
131
|
-
const message = this.messageQueue.shift()
|
|
132
|
-
this.send(message)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
disconnect() {
|
|
137
|
-
this.reconnect.cleanup()
|
|
138
|
-
if (this.ws) {
|
|
139
|
-
this.ws.close()
|
|
140
|
-
this.ws = null
|
|
141
|
-
}
|
|
142
|
-
this.state = 'disconnected'
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
on(event, handler) {
|
|
146
|
-
if (!this.listeners.has(event)) this.listeners.set(event, [])
|
|
147
|
-
this.listeners.get(event).push(handler)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
off(event, handler) {
|
|
151
|
-
if (!this.listeners.has(event)) return
|
|
152
|
-
const handlers = this.listeners.get(event)
|
|
153
|
-
const index = handlers.indexOf(handler)
|
|
154
|
-
if (index > -1) handlers.splice(index, 1)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
emit(event, data) {
|
|
158
|
-
if (!this.listeners.has(event)) return
|
|
159
|
-
for (const handler of this.listeners.get(event)) {
|
|
160
|
-
try {
|
|
161
|
-
handler(data)
|
|
162
|
-
} catch (error) {
|
|
163
|
-
log.error(`event handler error for ${event}:`, { message: error?.message || String(error) })
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
getState() {
|
|
169
|
-
return this.state
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
isConnected() {
|
|
173
|
-
return this.state === 'connected'
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export { StateTransportClient }
|
|
178
|
-
export default StateTransportClient
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// state-sync channel (reconnect logic): exponential-backoff + polling-fallback
|
|
2
|
-
// manager consumed by state-transport-client.js, part of the state-protocol.js
|
|
3
|
-
// quartet. See the header comment in state-protocol.js for the full
|
|
4
|
-
// ws-broadcast vs state-sync distinction. Zero real importers repo-wide
|
|
5
|
-
// outside this quartet's own internal cross-imports as of this writing.
|
|
6
|
-
import Protocol from './state-protocol.js'
|
|
7
|
-
|
|
8
|
-
const CONFIG = {
|
|
9
|
-
reconnectInitialDelay: 1000,
|
|
10
|
-
reconnectMaxDelay: 30000,
|
|
11
|
-
reconnectBackoffFactor: 2,
|
|
12
|
-
pingInterval: 25000,
|
|
13
|
-
connectionTimeout: 10000,
|
|
14
|
-
pollingInterval: 5000,
|
|
15
|
-
pollingFallbackDelay: 3000
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class ReconnectManager {
|
|
19
|
-
constructor(config, callbacks) {
|
|
20
|
-
this.config = config
|
|
21
|
-
this.callbacks = callbacks
|
|
22
|
-
this.reconnectAttempts = 0
|
|
23
|
-
this.reconnectTimer = null
|
|
24
|
-
this.pingTimer = null
|
|
25
|
-
this.pollingTimer = null
|
|
26
|
-
this.lastPongTime = 0
|
|
27
|
-
this.useFallback = false
|
|
28
|
-
this.wsUrl = null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get shouldFallback() {
|
|
32
|
-
return this.reconnectAttempts >= 3 && !this.useFallback
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
resetAttempts() {
|
|
36
|
-
this.reconnectAttempts = 0
|
|
37
|
-
this.useFallback = false
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
recordPong() {
|
|
41
|
-
this.lastPongTime = Date.now()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
scheduleReconnect() {
|
|
45
|
-
if (this.reconnectTimer) {
|
|
46
|
-
clearTimeout(this.reconnectTimer)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const delay = Math.min(
|
|
50
|
-
this.config.reconnectInitialDelay * Math.pow(this.config.reconnectBackoffFactor, this.reconnectAttempts),
|
|
51
|
-
this.config.reconnectMaxDelay
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
const jitter = Math.random() * 1000
|
|
55
|
-
this.reconnectAttempts++
|
|
56
|
-
|
|
57
|
-
this.reconnectTimer = setTimeout(() => {
|
|
58
|
-
this.callbacks.onReconnecting({ attempt: this.reconnectAttempts })
|
|
59
|
-
}, delay + jitter)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
activateFallback() {
|
|
63
|
-
this.useFallback = true
|
|
64
|
-
this.callbacks.onFallback('Switching to polling mode')
|
|
65
|
-
this.startPolling()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
startPolling() {
|
|
69
|
-
if (this.pollingTimer) return
|
|
70
|
-
|
|
71
|
-
this.pollingTimer = setInterval(async () => {
|
|
72
|
-
try {
|
|
73
|
-
this.callbacks.onPollStart()
|
|
74
|
-
const result = await this.pollState()
|
|
75
|
-
this.callbacks.onPollSuccess(result)
|
|
76
|
-
} catch (error) {
|
|
77
|
-
this.callbacks.onPollError(error)
|
|
78
|
-
}
|
|
79
|
-
}, this.config.pollingInterval)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async pollState() {
|
|
83
|
-
const httpUrl = this.wsUrl.replace('ws://', 'http://').replace('wss://', 'https://')
|
|
84
|
-
const response = await fetch(`${httpUrl}/poll`)
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
throw new Error(`Polling failed: ${response.status}`)
|
|
87
|
-
}
|
|
88
|
-
return await response.json()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
startPingInterval(sendFn, closeFn) {
|
|
92
|
-
this.pingTimer = setInterval(() => {
|
|
93
|
-
sendFn(Protocol.createPing())
|
|
94
|
-
const timeSinceLastPong = Date.now() - this.lastPongTime
|
|
95
|
-
if (timeSinceLastPong > this.config.pingInterval * 2) {
|
|
96
|
-
closeFn()
|
|
97
|
-
}
|
|
98
|
-
}, this.config.pingInterval)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
stopPingInterval() {
|
|
102
|
-
if (this.pingTimer) {
|
|
103
|
-
clearInterval(this.pingTimer)
|
|
104
|
-
this.pingTimer = null
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
stopPolling() {
|
|
109
|
-
if (this.pollingTimer) {
|
|
110
|
-
clearInterval(this.pollingTimer)
|
|
111
|
-
this.pollingTimer = null
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
cleanup() {
|
|
116
|
-
if (this.reconnectTimer) {
|
|
117
|
-
clearTimeout(this.reconnectTimer)
|
|
118
|
-
this.reconnectTimer = null
|
|
119
|
-
}
|
|
120
|
-
this.stopPolling()
|
|
121
|
-
this.stopPingInterval()
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export { CONFIG, ReconnectManager }
|
|
126
|
-
export default ReconnectManager
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// state-sync channel (server side): real `ws`-backed WebSocketServer with
|
|
2
|
-
// connection-guard IP/rate limiting, part of the state-protocol.js quartet.
|
|
3
|
-
// See the header comment in state-protocol.js for the full picture -- this is
|
|
4
|
-
// a structured, reconnect-aware state-sync protocol, DIFFERENT from and not a
|
|
5
|
-
// replacement for realtime-server.js's simple in-process pub/sub
|
|
6
|
-
// (the "ws-broadcast" channel actually used by the CRUD write path). As of
|
|
7
|
-
// this writing this file has zero real importers repo-wide; nothing
|
|
8
|
-
// constructs a StateTransportServer today.
|
|
9
|
-
import { WebSocketServer } from 'ws'
|
|
10
|
-
import { EventEmitter } from 'events'
|
|
11
|
-
import Protocol from '@/lib/state-protocol.js'
|
|
12
|
-
import ConnectionGuard from '@/lib/connection-guard.js'
|
|
13
|
-
|
|
14
|
-
const CONFIG = {
|
|
15
|
-
pingInterval: 30000,
|
|
16
|
-
connectionTimeout: 60000,
|
|
17
|
-
maxConnectionsPerIP: 10,
|
|
18
|
-
messageRateLimit: 100
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
class StateTransportServer extends EventEmitter {
|
|
22
|
-
constructor(server, config = {}) {
|
|
23
|
-
super()
|
|
24
|
-
this.config = { ...CONFIG, ...config }
|
|
25
|
-
this.wss = null
|
|
26
|
-
this.clients = new Map()
|
|
27
|
-
this.server = server
|
|
28
|
-
this.guard = new ConnectionGuard(this.config)
|
|
29
|
-
this.setupServer()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setupServer() {
|
|
33
|
-
try {
|
|
34
|
-
this.wss = new WebSocketServer({ server: this.server, path: '/state-sync' })
|
|
35
|
-
this.wss.on('connection', (ws, req) => this.handleConnection(ws, req))
|
|
36
|
-
this.wss.on('error', (error) => this.handleError(error))
|
|
37
|
-
this.guard.startPingInterval(this.clients, (id) => this.handleClose(id))
|
|
38
|
-
this.emit('ready')
|
|
39
|
-
} catch (error) {
|
|
40
|
-
this.emit('error', error)
|
|
41
|
-
setTimeout(() => this.setupServer(), 5000)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
handleConnection(ws, req) {
|
|
46
|
-
const clientId = this.guard.generateClientId()
|
|
47
|
-
const ip = req.socket.remoteAddress
|
|
48
|
-
|
|
49
|
-
if (!this.guard.checkConnectionLimit(ip)) {
|
|
50
|
-
ws.close(1008, 'Too many connections from this IP')
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const client = {
|
|
55
|
-
id: clientId,
|
|
56
|
-
ws,
|
|
57
|
-
ip,
|
|
58
|
-
alive: true,
|
|
59
|
-
connectedAt: Date.now(),
|
|
60
|
-
messageCount: 0
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
this.clients.set(clientId, client)
|
|
64
|
-
this.guard.trackIPConnection(ip)
|
|
65
|
-
|
|
66
|
-
ws.on('message', (data) => this.handleMessage(clientId, data))
|
|
67
|
-
ws.on('close', () => this.handleClose(clientId))
|
|
68
|
-
ws.on('error', (error) => this.handleClientError(clientId, error))
|
|
69
|
-
ws.on('pong', () => { client.alive = true })
|
|
70
|
-
|
|
71
|
-
this.emit('client_connected', clientId)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
handleMessage(clientId, data) {
|
|
75
|
-
try {
|
|
76
|
-
const client = this.clients.get(clientId)
|
|
77
|
-
if (!client) return
|
|
78
|
-
|
|
79
|
-
if (!this.guard.checkRateLimit(clientId)) {
|
|
80
|
-
this.sendMessage(clientId, Protocol.createStateNack(
|
|
81
|
-
null,
|
|
82
|
-
'Rate limit exceeded',
|
|
83
|
-
Protocol.ERROR_CODES.RATE_LIMITED
|
|
84
|
-
))
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const message = JSON.parse(data.toString())
|
|
89
|
-
const validation = Protocol.MessageSchema.validate(message)
|
|
90
|
-
|
|
91
|
-
if (!validation.valid) {
|
|
92
|
-
this.sendMessage(clientId, Protocol.createStateNack(
|
|
93
|
-
message.id,
|
|
94
|
-
validation.error,
|
|
95
|
-
Protocol.ERROR_CODES.INVALID_MESSAGE
|
|
96
|
-
))
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
client.messageCount++
|
|
101
|
-
this.emit('message', clientId, message)
|
|
102
|
-
|
|
103
|
-
if (message.type === Protocol.MESSAGE_TYPES.PING) {
|
|
104
|
-
this.sendMessage(clientId, Protocol.createPong(message.timestamp))
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
this.emit('error', { clientId, error, phase: 'message_handling' })
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
handleClose(clientId) {
|
|
112
|
-
const client = this.clients.get(clientId)
|
|
113
|
-
if (client) {
|
|
114
|
-
this.guard.untrackIPConnection(client.ip)
|
|
115
|
-
this.clients.delete(clientId)
|
|
116
|
-
this.guard.clearRateLimit(clientId)
|
|
117
|
-
this.emit('client_disconnected', clientId)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
handleClientError(clientId, error) {
|
|
122
|
-
this.emit('error', { clientId, error, phase: 'client_error' })
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
handleError(error) {
|
|
126
|
-
this.emit('error', { error, phase: 'server_error' })
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
sendMessage(clientId, message) {
|
|
130
|
-
try {
|
|
131
|
-
const client = this.clients.get(clientId)
|
|
132
|
-
if (client && client.ws.readyState === 1) {
|
|
133
|
-
client.ws.send(JSON.stringify(message))
|
|
134
|
-
return true
|
|
135
|
-
}
|
|
136
|
-
return false
|
|
137
|
-
} catch (error) {
|
|
138
|
-
this.emit('error', { clientId, error, phase: 'send_message' })
|
|
139
|
-
return false
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
broadcast(message, excludeClientId = null) {
|
|
144
|
-
const results = { sent: 0, failed: 0 }
|
|
145
|
-
for (const [clientId] of this.clients) {
|
|
146
|
-
if (clientId === excludeClientId) continue
|
|
147
|
-
if (this.sendMessage(clientId, message)) {
|
|
148
|
-
results.sent++
|
|
149
|
-
} else {
|
|
150
|
-
results.failed++
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return results
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
getClientCount() {
|
|
157
|
-
return this.clients.size
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
getClient(clientId) {
|
|
161
|
-
return this.clients.get(clientId)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
close() {
|
|
165
|
-
this.guard.destroy()
|
|
166
|
-
for (const client of this.clients.values()) {
|
|
167
|
-
client.ws.close()
|
|
168
|
-
}
|
|
169
|
-
this.clients.clear()
|
|
170
|
-
if (this.wss) {
|
|
171
|
-
this.wss.close()
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (!global.stateTransportServer) {
|
|
177
|
-
global.stateTransportServer = null
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function createStateTransportServer(server, config) {
|
|
181
|
-
if (global.stateTransportServer) {
|
|
182
|
-
global.stateTransportServer.close()
|
|
183
|
-
}
|
|
184
|
-
global.stateTransportServer = new StateTransportServer(server, config)
|
|
185
|
-
return global.stateTransportServer
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export { StateTransportServer }
|
|
189
|
-
export default StateTransportServer
|
package/src/lib/static-server.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const ROOT = path.join(__dirname, '../..');
|
|
7
|
-
|
|
8
|
-
export function serveStatic(pathname, req, res, compress, getCacheHeaders, _loadModule) {
|
|
9
|
-
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
10
|
-
|
|
11
|
-
if (pathname === '/favicon.ico') {
|
|
12
|
-
res.setHeader('Content-Type', 'image/x-icon');
|
|
13
|
-
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
14
|
-
res.setHeader('Content-Length', '0');
|
|
15
|
-
res.writeHead(200);
|
|
16
|
-
res.end();
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (pathname === '/manifest.json') {
|
|
21
|
-
const manifest = JSON.stringify({ name: 'Thatcher', short_name: 'Thatcher', start_url: '/', display: 'standalone', background_color: '#f1f5f9', theme_color: '#04141f' });
|
|
22
|
-
res.setHeader('Content-Type', 'application/json');
|
|
23
|
-
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
24
|
-
res.setHeader('Content-Length', Buffer.byteLength(manifest, 'utf-8'));
|
|
25
|
-
res.writeHead(200);
|
|
26
|
-
res.end(manifest);
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (pathname === '/service-worker.js') {
|
|
31
|
-
const swPath = path.join(ROOT, 'src/service-worker.js');
|
|
32
|
-
if (fs.existsSync(swPath)) {
|
|
33
|
-
const content = fs.readFileSync(swPath, 'utf-8');
|
|
34
|
-
const cacheHeaders = getCacheHeaders('dynamic');
|
|
35
|
-
Object.entries(cacheHeaders).forEach(([k, v]) => res.setHeader(k, v));
|
|
36
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
37
|
-
res.setHeader('Content-Length', Buffer.byteLength(content, 'utf-8'));
|
|
38
|
-
res.writeHead(200);
|
|
39
|
-
res.end(content);
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (pathname.startsWith('/lib/webjsx/')) {
|
|
45
|
-
const file = pathname.slice(12);
|
|
46
|
-
const filePath = path.join(ROOT, 'node_modules/webjsx/dist', file);
|
|
47
|
-
if (!fs.existsSync(filePath)) return false;
|
|
48
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
49
|
-
const cacheHeaders = getCacheHeaders('static', 31536000);
|
|
50
|
-
Object.entries(cacheHeaders).forEach(([k, v]) => res.setHeader(k, v));
|
|
51
|
-
const { content: finalContent, encoding } = compress(content, acceptEncoding);
|
|
52
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
53
|
-
if (encoding) res.setHeader('Content-Encoding', encoding);
|
|
54
|
-
res.setHeader('Content-Length', Buffer.byteLength(finalContent));
|
|
55
|
-
res.writeHead(200);
|
|
56
|
-
res.end(finalContent);
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (pathname.startsWith('/ui/') && pathname.endsWith('.css')) {
|
|
61
|
-
const cssPath = path.join(ROOT, 'src/ui', path.basename(pathname));
|
|
62
|
-
if (!fs.existsSync(cssPath)) return false;
|
|
63
|
-
let content = fs.readFileSync(cssPath, 'utf-8');
|
|
64
|
-
const etag = `"${content.length}-${fs.statSync(cssPath).mtimeMs.toString(36)}"`;
|
|
65
|
-
if (req.headers['if-none-match'] === etag) { res.writeHead(304); res.end(); return true; }
|
|
66
|
-
res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
67
|
-
res.setHeader('ETag', etag);
|
|
68
|
-
const { content: finalContent, encoding } = compress(content, acceptEncoding);
|
|
69
|
-
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
70
|
-
if (encoding) res.setHeader('Content-Encoding', encoding);
|
|
71
|
-
res.setHeader('Content-Length', Buffer.byteLength(finalContent));
|
|
72
|
-
res.writeHead(200);
|
|
73
|
-
res.end(finalContent);
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (pathname === '/ui/client.js' || pathname === '/ui/event-delegation.js' || pathname === '/ui/common-handlers.js') {
|
|
78
|
-
const jsPath = path.join(ROOT, 'src/ui', pathname.split('/').pop());
|
|
79
|
-
if (!fs.existsSync(jsPath)) return false;
|
|
80
|
-
const content = fs.readFileSync(jsPath, 'utf-8');
|
|
81
|
-
const cacheHeaders = getCacheHeaders('static', 86400);
|
|
82
|
-
Object.entries(cacheHeaders).forEach(([k, v]) => res.setHeader(k, v));
|
|
83
|
-
const { content: finalContent, encoding } = compress(content, acceptEncoding);
|
|
84
|
-
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
85
|
-
if (encoding) res.setHeader('Content-Encoding', encoding);
|
|
86
|
-
res.setHeader('Content-Length', Buffer.byteLength(finalContent));
|
|
87
|
-
res.writeHead(200);
|
|
88
|
-
res.end(finalContent);
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function html404() {
|
|
96
|
-
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><script>(function(){try{var t=localStorage.getItem('thatcher-theme')||((window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light');document.documentElement.setAttribute('data-theme',t)}catch(e){document.documentElement.setAttribute('data-theme','light')}})();</script><meta name="viewport" content="width=device-width,initial-scale=1"><title>404 - Page Not Found | Thatcher</title><link href="/ui/rippleui.css" rel="stylesheet"><link href="/ui/styles2.css" rel="stylesheet"><style>body{margin:0;background:var(--color-bg,#f1f5f9);font-family:system-ui,sans-serif}.nav-shell{background:#04141f;padding:0 2rem;height:56px;display:flex;align-items:center}a.logo-link{color:#fff;text-decoration:none;font-weight:700;font-size:1.1rem}.error-shell{min-height:calc(100vh - 56px);display:flex;align-items:center;justify-content:center}.error-card{background:#fff;border-radius:12px;padding:3rem 4rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)}.error-code{font-size:4rem;font-weight:900;color:#04141f;line-height:1}.error-msg{font-size:1.2rem;color:#64748b;margin:0.5rem 0 2rem}.home-btn{display:inline-block;padding:0.75rem 2rem;background:#04141f;color:#fff;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.95rem}</style></head><body><nav class="nav-shell"><a href="/" class="logo-link">Thatcher</a></nav><div class="error-shell"><div class="error-card"><div class="error-code">404</div><p class="error-msg">Page not found</p><a href="/" class="home-btn">Go to Dashboard</a></div></div></body></html>`;
|
|
97
|
-
}
|