thatcher 1.0.4

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. package/src/ui/widgets.js +152 -0
@@ -0,0 +1,171 @@
1
+ const MESSAGE_TYPES = {
2
+ STATE_UPDATE: 'state_update',
3
+ STATE_SNAPSHOT: 'state_snapshot',
4
+ STATE_REQUEST: 'state_request',
5
+ STATE_ACK: 'state_ack',
6
+ STATE_NACK: 'state_nack',
7
+ PING: 'ping',
8
+ PONG: 'pong'
9
+ }
10
+
11
+ const ERROR_CODES = {
12
+ INVALID_MESSAGE: 'invalid_message',
13
+ INVALID_VERSION: 'invalid_version',
14
+ CONFLICT_DETECTED: 'conflict_detected',
15
+ RATE_LIMITED: 'rate_limited',
16
+ VALIDATION_FAILED: 'validation_failed'
17
+ }
18
+
19
+ class VectorClock {
20
+ constructor(nodeId) {
21
+ this.nodeId = nodeId
22
+ this.clock = { [nodeId]: 0 }
23
+ }
24
+
25
+ increment() {
26
+ this.clock[this.nodeId] = (this.clock[this.nodeId] || 0) + 1
27
+ return this.clock[this.nodeId]
28
+ }
29
+
30
+ update(otherClock) {
31
+ for (const [nodeId, timestamp] of Object.entries(otherClock)) {
32
+ this.clock[nodeId] = Math.max(this.clock[nodeId] || 0, timestamp)
33
+ }
34
+ }
35
+
36
+ compare(otherClock) {
37
+ const keys = new Set([...Object.keys(this.clock), ...Object.keys(otherClock)])
38
+ let hasGreater = false
39
+ let hasLess = false
40
+
41
+ for (const key of keys) {
42
+ const mine = this.clock[key] || 0
43
+ const theirs = otherClock[key] || 0
44
+ if (mine > theirs) hasGreater = true
45
+ if (mine < theirs) hasLess = true
46
+ }
47
+
48
+ if (hasGreater && !hasLess) return 1
49
+ if (hasLess && !hasGreater) return -1
50
+ if (!hasGreater && !hasLess) return 0
51
+ return null // Concurrent
52
+ }
53
+
54
+ serialize() {
55
+ return { ...this.clock }
56
+ }
57
+
58
+ static deserialize(data, nodeId) {
59
+ const vc = new VectorClock(nodeId)
60
+ vc.clock = { ...data }
61
+ return vc
62
+ }
63
+ }
64
+
65
+ const MessageSchema = {
66
+ validate(message) {
67
+ if (!message || typeof message !== 'object') {
68
+ return { valid: false, error: 'Message must be an object' }
69
+ }
70
+ const validTypes = Object.values(MESSAGE_TYPES)
71
+ if (!validTypes.includes(message.type)) {
72
+ return { valid: false, error: 'Invalid message type' }
73
+ }
74
+ if (message.type !== MESSAGE_TYPES.PING && message.type !== MESSAGE_TYPES.PONG) {
75
+ if (!message.version || typeof message.version !== 'object') {
76
+ return { valid: false, error: 'Missing or invalid version vector' }
77
+ }
78
+ }
79
+ if (message.type === MESSAGE_TYPES.STATE_UPDATE || message.type === MESSAGE_TYPES.STATE_SNAPSHOT) {
80
+ if (message.data === undefined) {
81
+ return { valid: false, error: 'Missing data field' }
82
+ }
83
+ }
84
+ return { valid: true }
85
+ }
86
+ }
87
+
88
+ function createMessage(type, payload = {}) {
89
+ return {
90
+ type,
91
+ timestamp: Date.now(),
92
+ ...payload
93
+ }
94
+ }
95
+
96
+ function createStateUpdate(vectorClock, data, operations = []) {
97
+ return createMessage(MESSAGE_TYPES.STATE_UPDATE, {
98
+ version: vectorClock.serialize(),
99
+ data,
100
+ operations
101
+ })
102
+ }
103
+
104
+ function createStateSnapshot(vectorClock, data) {
105
+ return createMessage(MESSAGE_TYPES.STATE_SNAPSHOT, {
106
+ version: vectorClock.serialize(),
107
+ data
108
+ })
109
+ }
110
+
111
+ function createStateRequest(vectorClock) {
112
+ return createMessage(MESSAGE_TYPES.STATE_REQUEST, {
113
+ version: vectorClock.serialize()
114
+ })
115
+ }
116
+
117
+ function createStateAck(messageId, vectorClock) {
118
+ return createMessage(MESSAGE_TYPES.STATE_ACK, {
119
+ messageId,
120
+ version: vectorClock.serialize()
121
+ })
122
+ }
123
+
124
+ function createStateNack(messageId, error, errorCode) {
125
+ return createMessage(MESSAGE_TYPES.STATE_NACK, {
126
+ messageId,
127
+ error,
128
+ errorCode
129
+ })
130
+ }
131
+
132
+ function createPing() {
133
+ return createMessage(MESSAGE_TYPES.PING)
134
+ }
135
+
136
+ function createPong(pingTimestamp) {
137
+ return createMessage(MESSAGE_TYPES.PONG, { pingTimestamp })
138
+ }
139
+
140
+ const Protocol = {
141
+ MESSAGE_TYPES,
142
+ ERROR_CODES,
143
+ VectorClock,
144
+ MessageSchema,
145
+ createMessage,
146
+ createStateUpdate,
147
+ createStateSnapshot,
148
+ createStateRequest,
149
+ createStateAck,
150
+ createStateNack,
151
+ createPing,
152
+ createPong
153
+ }
154
+
155
+ export {
156
+ MESSAGE_TYPES,
157
+ ERROR_CODES,
158
+ VectorClock,
159
+ MessageSchema,
160
+ createMessage,
161
+ createStateUpdate,
162
+ createStateSnapshot,
163
+ createStateRequest,
164
+ createStateAck,
165
+ createStateNack,
166
+ createPing,
167
+ createPong,
168
+ Protocol
169
+ }
170
+
171
+ export default Protocol
@@ -0,0 +1,169 @@
1
+ import Protocol from './state-protocol.js'
2
+ import { CONFIG, ReconnectManager } from './state-transport-reconnect.js'
3
+
4
+ class StateTransportClient {
5
+ constructor(config = {}) {
6
+ this.config = { ...CONFIG, ...config }
7
+ this.ws = null
8
+ this.state = 'disconnected'
9
+ this.listeners = new Map()
10
+ this.messageQueue = []
11
+ this.connectionTimeoutTimer = null
12
+
13
+ this.reconnect = new ReconnectManager(this.config, {
14
+ onReconnecting: (data) => { this.emit('reconnecting', data); this.connect() },
15
+ onFallback: (msg) => this.emit('fallback', msg),
16
+ onPollStart: () => this.emit('poll_start'),
17
+ onPollSuccess: (result) => this.emit('poll_success', result),
18
+ onPollError: (error) => this.emit('poll_error', error)
19
+ })
20
+ }
21
+
22
+ connect(url) {
23
+ this.url = url || this.url
24
+ if (!this.url) {
25
+ this.emit('error', new Error('No URL provided for connection'))
26
+ return
27
+ }
28
+ this.reconnect.wsUrl = this.url
29
+
30
+ if (this.state === 'connecting' || this.state === 'connected') return
31
+
32
+ this.state = 'connecting'
33
+ this.emit('connecting')
34
+
35
+ try {
36
+ this.ws = new WebSocket(this.url)
37
+
38
+ this.connectionTimeoutTimer = setTimeout(() => {
39
+ if (this.state === 'connecting') this.handleConnectionTimeout()
40
+ }, this.config.connectionTimeout)
41
+
42
+ this.ws.onopen = () => this.handleOpen()
43
+ this.ws.onclose = (event) => this.handleClose(event)
44
+ this.ws.onerror = (error) => this.handleError(error)
45
+ this.ws.onmessage = (event) => this.handleMessage(event)
46
+ } catch (error) {
47
+ this.handleConnectionFailure(error)
48
+ }
49
+ }
50
+
51
+ handleOpen() {
52
+ clearTimeout(this.connectionTimeoutTimer)
53
+ this.state = 'connected'
54
+ this.reconnect.resetAttempts()
55
+ this.emit('connected')
56
+ this.reconnect.startPingInterval(
57
+ (msg) => this.send(msg),
58
+ () => this.ws.close()
59
+ )
60
+ this.flushMessageQueue()
61
+ this.reconnect.stopPolling()
62
+ }
63
+
64
+ handleClose(event) {
65
+ clearTimeout(this.connectionTimeoutTimer)
66
+ this.state = 'disconnected'
67
+ this.reconnect.stopPingInterval()
68
+ this.emit('disconnected', { code: event.code, reason: event.reason })
69
+ this.reconnect.scheduleReconnect()
70
+ }
71
+
72
+ handleError(error) {
73
+ this.emit('error', error)
74
+ }
75
+
76
+ handleConnectionTimeout() {
77
+ if (this.ws) this.ws.close()
78
+ this.handleConnectionFailure(new Error('Connection timeout'))
79
+ }
80
+
81
+ handleConnectionFailure(error) {
82
+ this.emit('error', error)
83
+ this.state = 'disconnected'
84
+
85
+ if (this.reconnect.shouldFallback) {
86
+ this.reconnect.activateFallback()
87
+ } else {
88
+ this.reconnect.scheduleReconnect()
89
+ }
90
+ }
91
+
92
+ handleMessage(event) {
93
+ try {
94
+ const message = JSON.parse(event.data)
95
+ if (message.type === Protocol.MESSAGE_TYPES.PONG) {
96
+ this.reconnect.recordPong()
97
+ return
98
+ }
99
+ this.emit('message', message)
100
+ } catch (error) {
101
+ this.emit('error', { error, phase: 'message_parsing', data: event.data })
102
+ }
103
+ }
104
+
105
+ send(message) {
106
+ if (this.state === 'connected' && this.ws && this.ws.readyState === WebSocket.OPEN) {
107
+ try {
108
+ this.ws.send(JSON.stringify(message))
109
+ return true
110
+ } catch (error) {
111
+ this.emit('error', { error, phase: 'send' })
112
+ this.messageQueue.push(message)
113
+ return false
114
+ }
115
+ }
116
+ this.messageQueue.push(message)
117
+ return false
118
+ }
119
+
120
+ flushMessageQueue() {
121
+ while (this.messageQueue.length > 0 && this.state === 'connected') {
122
+ const message = this.messageQueue.shift()
123
+ this.send(message)
124
+ }
125
+ }
126
+
127
+ disconnect() {
128
+ this.reconnect.cleanup()
129
+ if (this.ws) {
130
+ this.ws.close()
131
+ this.ws = null
132
+ }
133
+ this.state = 'disconnected'
134
+ }
135
+
136
+ on(event, handler) {
137
+ if (!this.listeners.has(event)) this.listeners.set(event, [])
138
+ this.listeners.get(event).push(handler)
139
+ }
140
+
141
+ off(event, handler) {
142
+ if (!this.listeners.has(event)) return
143
+ const handlers = this.listeners.get(event)
144
+ const index = handlers.indexOf(handler)
145
+ if (index > -1) handlers.splice(index, 1)
146
+ }
147
+
148
+ emit(event, data) {
149
+ if (!this.listeners.has(event)) return
150
+ for (const handler of this.listeners.get(event)) {
151
+ try {
152
+ handler(data)
153
+ } catch (error) {
154
+ console.error(`Error in event handler for ${event}:`, error)
155
+ }
156
+ }
157
+ }
158
+
159
+ getState() {
160
+ return this.state
161
+ }
162
+
163
+ isConnected() {
164
+ return this.state === 'connected'
165
+ }
166
+ }
167
+
168
+ export { StateTransportClient }
169
+ export default StateTransportClient
@@ -0,0 +1,121 @@
1
+ import Protocol from './state-protocol.js'
2
+
3
+ const CONFIG = {
4
+ reconnectInitialDelay: 1000,
5
+ reconnectMaxDelay: 30000,
6
+ reconnectBackoffFactor: 2,
7
+ pingInterval: 25000,
8
+ connectionTimeout: 10000,
9
+ pollingInterval: 5000,
10
+ pollingFallbackDelay: 3000
11
+ }
12
+
13
+ class ReconnectManager {
14
+ constructor(config, callbacks) {
15
+ this.config = config
16
+ this.callbacks = callbacks
17
+ this.reconnectAttempts = 0
18
+ this.reconnectTimer = null
19
+ this.pingTimer = null
20
+ this.pollingTimer = null
21
+ this.lastPongTime = 0
22
+ this.useFallback = false
23
+ this.wsUrl = null
24
+ }
25
+
26
+ get shouldFallback() {
27
+ return this.reconnectAttempts >= 3 && !this.useFallback
28
+ }
29
+
30
+ resetAttempts() {
31
+ this.reconnectAttempts = 0
32
+ this.useFallback = false
33
+ }
34
+
35
+ recordPong() {
36
+ this.lastPongTime = Date.now()
37
+ }
38
+
39
+ scheduleReconnect() {
40
+ if (this.reconnectTimer) {
41
+ clearTimeout(this.reconnectTimer)
42
+ }
43
+
44
+ const delay = Math.min(
45
+ this.config.reconnectInitialDelay * Math.pow(this.config.reconnectBackoffFactor, this.reconnectAttempts),
46
+ this.config.reconnectMaxDelay
47
+ )
48
+
49
+ const jitter = Math.random() * 1000
50
+ this.reconnectAttempts++
51
+
52
+ this.reconnectTimer = setTimeout(() => {
53
+ this.callbacks.onReconnecting({ attempt: this.reconnectAttempts })
54
+ }, delay + jitter)
55
+ }
56
+
57
+ activateFallback() {
58
+ this.useFallback = true
59
+ this.callbacks.onFallback('Switching to polling mode')
60
+ this.startPolling()
61
+ }
62
+
63
+ startPolling() {
64
+ if (this.pollingTimer) return
65
+
66
+ this.pollingTimer = setInterval(async () => {
67
+ try {
68
+ this.callbacks.onPollStart()
69
+ const result = await this.pollState()
70
+ this.callbacks.onPollSuccess(result)
71
+ } catch (error) {
72
+ this.callbacks.onPollError(error)
73
+ }
74
+ }, this.config.pollingInterval)
75
+ }
76
+
77
+ async pollState() {
78
+ const httpUrl = this.wsUrl.replace('ws://', 'http://').replace('wss://', 'https://')
79
+ const response = await fetch(`${httpUrl}/poll`)
80
+ if (!response.ok) {
81
+ throw new Error(`Polling failed: ${response.status}`)
82
+ }
83
+ return await response.json()
84
+ }
85
+
86
+ startPingInterval(sendFn, closeFn) {
87
+ this.pingTimer = setInterval(() => {
88
+ sendFn(Protocol.createPing())
89
+ const timeSinceLastPong = Date.now() - this.lastPongTime
90
+ if (timeSinceLastPong > this.config.pingInterval * 2) {
91
+ closeFn()
92
+ }
93
+ }, this.config.pingInterval)
94
+ }
95
+
96
+ stopPingInterval() {
97
+ if (this.pingTimer) {
98
+ clearInterval(this.pingTimer)
99
+ this.pingTimer = null
100
+ }
101
+ }
102
+
103
+ stopPolling() {
104
+ if (this.pollingTimer) {
105
+ clearInterval(this.pollingTimer)
106
+ this.pollingTimer = null
107
+ }
108
+ }
109
+
110
+ cleanup() {
111
+ if (this.reconnectTimer) {
112
+ clearTimeout(this.reconnectTimer)
113
+ this.reconnectTimer = null
114
+ }
115
+ this.stopPolling()
116
+ this.stopPingInterval()
117
+ }
118
+ }
119
+
120
+ export { CONFIG, ReconnectManager }
121
+ export default ReconnectManager
@@ -0,0 +1,181 @@
1
+ import { WebSocketServer } from 'ws'
2
+ import { EventEmitter } from 'events'
3
+ import Protocol from '@/lib/state-protocol.js'
4
+ import ConnectionGuard from '@/lib/connection-guard.js'
5
+
6
+ const CONFIG = {
7
+ pingInterval: 30000,
8
+ connectionTimeout: 60000,
9
+ maxConnectionsPerIP: 10,
10
+ messageRateLimit: 100
11
+ }
12
+
13
+ class StateTransportServer extends EventEmitter {
14
+ constructor(server, config = {}) {
15
+ super()
16
+ this.config = { ...CONFIG, ...config }
17
+ this.wss = null
18
+ this.clients = new Map()
19
+ this.server = server
20
+ this.guard = new ConnectionGuard(this.config)
21
+ this.setupServer()
22
+ }
23
+
24
+ setupServer() {
25
+ try {
26
+ this.wss = new WebSocketServer({ server: this.server, path: '/state-sync' })
27
+ this.wss.on('connection', (ws, req) => this.handleConnection(ws, req))
28
+ this.wss.on('error', (error) => this.handleError(error))
29
+ this.guard.startPingInterval(this.clients, (id) => this.handleClose(id))
30
+ this.emit('ready')
31
+ } catch (error) {
32
+ this.emit('error', error)
33
+ setTimeout(() => this.setupServer(), 5000)
34
+ }
35
+ }
36
+
37
+ handleConnection(ws, req) {
38
+ const clientId = this.guard.generateClientId()
39
+ const ip = req.socket.remoteAddress
40
+
41
+ if (!this.guard.checkConnectionLimit(ip)) {
42
+ ws.close(1008, 'Too many connections from this IP')
43
+ return
44
+ }
45
+
46
+ const client = {
47
+ id: clientId,
48
+ ws,
49
+ ip,
50
+ alive: true,
51
+ connectedAt: Date.now(),
52
+ messageCount: 0
53
+ }
54
+
55
+ this.clients.set(clientId, client)
56
+ this.guard.trackIPConnection(ip)
57
+
58
+ ws.on('message', (data) => this.handleMessage(clientId, data))
59
+ ws.on('close', () => this.handleClose(clientId))
60
+ ws.on('error', (error) => this.handleClientError(clientId, error))
61
+ ws.on('pong', () => { client.alive = true })
62
+
63
+ this.emit('client_connected', clientId)
64
+ }
65
+
66
+ handleMessage(clientId, data) {
67
+ try {
68
+ const client = this.clients.get(clientId)
69
+ if (!client) return
70
+
71
+ if (!this.guard.checkRateLimit(clientId)) {
72
+ this.sendMessage(clientId, Protocol.createStateNack(
73
+ null,
74
+ 'Rate limit exceeded',
75
+ Protocol.ERROR_CODES.RATE_LIMITED
76
+ ))
77
+ return
78
+ }
79
+
80
+ const message = JSON.parse(data.toString())
81
+ const validation = Protocol.MessageSchema.validate(message)
82
+
83
+ if (!validation.valid) {
84
+ this.sendMessage(clientId, Protocol.createStateNack(
85
+ message.id,
86
+ validation.error,
87
+ Protocol.ERROR_CODES.INVALID_MESSAGE
88
+ ))
89
+ return
90
+ }
91
+
92
+ client.messageCount++
93
+ this.emit('message', clientId, message)
94
+
95
+ if (message.type === Protocol.MESSAGE_TYPES.PING) {
96
+ this.sendMessage(clientId, Protocol.createPong(message.timestamp))
97
+ }
98
+ } catch (error) {
99
+ this.emit('error', { clientId, error, phase: 'message_handling' })
100
+ }
101
+ }
102
+
103
+ handleClose(clientId) {
104
+ const client = this.clients.get(clientId)
105
+ if (client) {
106
+ this.guard.untrackIPConnection(client.ip)
107
+ this.clients.delete(clientId)
108
+ this.guard.clearRateLimit(clientId)
109
+ this.emit('client_disconnected', clientId)
110
+ }
111
+ }
112
+
113
+ handleClientError(clientId, error) {
114
+ this.emit('error', { clientId, error, phase: 'client_error' })
115
+ }
116
+
117
+ handleError(error) {
118
+ this.emit('error', { error, phase: 'server_error' })
119
+ }
120
+
121
+ sendMessage(clientId, message) {
122
+ try {
123
+ const client = this.clients.get(clientId)
124
+ if (client && client.ws.readyState === 1) {
125
+ client.ws.send(JSON.stringify(message))
126
+ return true
127
+ }
128
+ return false
129
+ } catch (error) {
130
+ this.emit('error', { clientId, error, phase: 'send_message' })
131
+ return false
132
+ }
133
+ }
134
+
135
+ broadcast(message, excludeClientId = null) {
136
+ const results = { sent: 0, failed: 0 }
137
+ for (const [clientId] of this.clients) {
138
+ if (clientId === excludeClientId) continue
139
+ if (this.sendMessage(clientId, message)) {
140
+ results.sent++
141
+ } else {
142
+ results.failed++
143
+ }
144
+ }
145
+ return results
146
+ }
147
+
148
+ getClientCount() {
149
+ return this.clients.size
150
+ }
151
+
152
+ getClient(clientId) {
153
+ return this.clients.get(clientId)
154
+ }
155
+
156
+ close() {
157
+ this.guard.destroy()
158
+ for (const client of this.clients.values()) {
159
+ client.ws.close()
160
+ }
161
+ this.clients.clear()
162
+ if (this.wss) {
163
+ this.wss.close()
164
+ }
165
+ }
166
+ }
167
+
168
+ if (!global.stateTransportServer) {
169
+ global.stateTransportServer = null
170
+ }
171
+
172
+ export function createStateTransportServer(server, config) {
173
+ if (global.stateTransportServer) {
174
+ global.stateTransportServer.close()
175
+ }
176
+ global.stateTransportServer = new StateTransportServer(server, config)
177
+ return global.stateTransportServer
178
+ }
179
+
180
+ export { StateTransportServer }
181
+ export default StateTransportServer