@talex-touch/utils 1.0.40 → 1.0.44

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 (235) hide show
  1. package/.eslintcache +1 -0
  2. package/__tests__/cloud-sync-sdk.test.ts +442 -0
  3. package/__tests__/icons/icons.test.ts +84 -0
  4. package/__tests__/plugin-sdk-lifecycle.test.ts +130 -0
  5. package/__tests__/power-sdk.test.ts +143 -0
  6. package/__tests__/preset-export-types.test.ts +108 -0
  7. package/__tests__/search/fuzzy-match.test.ts +137 -0
  8. package/__tests__/transport/port-policy.test.ts +44 -0
  9. package/__tests__/transport-domain-sdks.test.ts +152 -0
  10. package/__tests__/types/update.test.ts +67 -0
  11. package/account/account-sdk.ts +915 -0
  12. package/account/index.ts +2 -0
  13. package/account/types.ts +321 -0
  14. package/analytics/client.ts +136 -0
  15. package/analytics/index.ts +2 -0
  16. package/analytics/types.ts +156 -0
  17. package/animation/auto-resize.ts +322 -0
  18. package/animation/window-node.ts +26 -19
  19. package/auth/clerk-types.ts +12 -30
  20. package/auth/index.ts +0 -2
  21. package/auth/useAuthState.ts +6 -14
  22. package/base/index.ts +2 -0
  23. package/base/log-level.ts +105 -0
  24. package/channel/index.ts +170 -69
  25. package/cloud-sync/cloud-sync-sdk.ts +450 -0
  26. package/cloud-sync/index.ts +1 -0
  27. package/common/file-scan-utils.ts +17 -9
  28. package/common/index.ts +4 -0
  29. package/common/logger/index.ts +46 -0
  30. package/common/logger/logger-manager.ts +303 -0
  31. package/common/logger/module-logger.ts +270 -0
  32. package/common/logger/transport-logger.ts +234 -0
  33. package/common/logger/types.ts +93 -0
  34. package/common/search/gather.ts +48 -6
  35. package/common/search/index.ts +8 -0
  36. package/common/storage/constants.ts +13 -0
  37. package/common/storage/entity/app-settings.ts +245 -0
  38. package/common/storage/entity/index.ts +3 -0
  39. package/common/storage/entity/layout-atom-types.ts +147 -0
  40. package/common/storage/entity/openers.ts +1 -0
  41. package/common/storage/entity/preset-cloud-api.ts +132 -0
  42. package/common/storage/entity/preset-export-types.ts +256 -0
  43. package/common/storage/entity/shortcut-settings.ts +1 -0
  44. package/common/storage/shortcut-storage.ts +11 -0
  45. package/common/utils/clone-diagnostics.ts +105 -0
  46. package/common/utils/file.ts +16 -8
  47. package/common/utils/index.ts +6 -2
  48. package/common/utils/payload-preview.ts +173 -0
  49. package/common/utils/polling.ts +167 -13
  50. package/common/utils/safe-path.ts +103 -0
  51. package/common/utils/safe-shell.ts +115 -0
  52. package/common/utils/task-queue.ts +4 -1
  53. package/core-box/builder/tuff-builder.ts +0 -1
  54. package/core-box/index.ts +1 -1
  55. package/core-box/recommendation.ts +38 -1
  56. package/core-box/tuff/tuff-dsl.ts +97 -0
  57. package/electron/download-manager.ts +10 -7
  58. package/electron/env-tool.ts +42 -40
  59. package/electron/index.ts +0 -1
  60. package/env/index.ts +156 -0
  61. package/eslint.config.js +55 -0
  62. package/i18n/index.ts +62 -0
  63. package/i18n/locales/en.json +226 -0
  64. package/i18n/locales/zh.json +226 -0
  65. package/i18n/message-keys.ts +236 -0
  66. package/i18n/resolver.ts +181 -0
  67. package/icons/index.ts +257 -0
  68. package/icons/svg.ts +69 -0
  69. package/index.ts +9 -1
  70. package/intelligence/client.ts +72 -42
  71. package/market/constants.ts +21 -3
  72. package/market/index.ts +1 -1
  73. package/market/types.ts +20 -5
  74. package/package.json +15 -5
  75. package/permission/index.ts +143 -46
  76. package/permission/legacy.ts +26 -0
  77. package/permission/registry.ts +304 -0
  78. package/permission/types.ts +164 -0
  79. package/plugin/channel.ts +68 -39
  80. package/plugin/index.ts +82 -8
  81. package/plugin/install.ts +3 -0
  82. package/plugin/log/types.ts +22 -5
  83. package/plugin/node/logger-manager.ts +11 -3
  84. package/plugin/node/logger.ts +24 -17
  85. package/plugin/preload.ts +25 -2
  86. package/plugin/providers/index.ts +4 -0
  87. package/plugin/providers/market-client.ts +218 -0
  88. package/plugin/providers/npm-provider.ts +228 -0
  89. package/plugin/providers/tpex-provider.ts +297 -0
  90. package/plugin/providers/tpex-types.ts +34 -0
  91. package/plugin/sdk/box-items.ts +14 -0
  92. package/plugin/sdk/box-sdk.ts +64 -0
  93. package/plugin/sdk/channel.ts +119 -4
  94. package/plugin/sdk/clipboard.ts +26 -12
  95. package/plugin/sdk/cloud-sync.ts +113 -0
  96. package/plugin/sdk/common.ts +19 -11
  97. package/plugin/sdk/core-box.ts +6 -15
  98. package/plugin/sdk/division-box.ts +160 -65
  99. package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
  100. package/plugin/sdk/feature-sdk.ts +111 -76
  101. package/plugin/sdk/flow.ts +146 -45
  102. package/plugin/sdk/hooks/bridge.ts +113 -49
  103. package/plugin/sdk/hooks/life-cycle.ts +35 -16
  104. package/plugin/sdk/index.ts +14 -3
  105. package/plugin/sdk/intelligence.ts +87 -0
  106. package/plugin/sdk/meta/README.md +179 -0
  107. package/plugin/sdk/meta-sdk.ts +244 -0
  108. package/plugin/sdk/notification.ts +9 -0
  109. package/plugin/sdk/performance.ts +1 -16
  110. package/plugin/sdk/plugin-info.ts +64 -0
  111. package/plugin/sdk/power.ts +155 -0
  112. package/plugin/sdk/recommend.ts +21 -0
  113. package/plugin/sdk/service/index.ts +12 -8
  114. package/plugin/sdk/sqlite.ts +141 -0
  115. package/plugin/sdk/storage.ts +2 -6
  116. package/plugin/sdk/system.ts +2 -9
  117. package/plugin/sdk/temp-files.ts +41 -0
  118. package/plugin/sdk/touch-sdk.ts +18 -0
  119. package/plugin/sdk/types.ts +44 -4
  120. package/plugin/sdk/window/index.ts +12 -9
  121. package/plugin/sdk-version.ts +231 -0
  122. package/preload/renderer.ts +3 -2
  123. package/renderer/hooks/arg-mapper.ts +34 -6
  124. package/renderer/hooks/index.ts +13 -0
  125. package/renderer/hooks/initialize.ts +2 -1
  126. package/renderer/hooks/use-agent-market-sdk.ts +7 -0
  127. package/renderer/hooks/use-agent-market.ts +106 -0
  128. package/renderer/hooks/use-agents-sdk.ts +7 -0
  129. package/renderer/hooks/use-app-sdk.ts +7 -0
  130. package/renderer/hooks/use-channel.ts +33 -4
  131. package/renderer/hooks/use-download-sdk.ts +21 -0
  132. package/renderer/hooks/use-intelligence-sdk.ts +7 -0
  133. package/renderer/hooks/use-intelligence-stats.ts +290 -0
  134. package/renderer/hooks/use-intelligence.ts +202 -104
  135. package/renderer/hooks/use-market-sdk.ts +16 -0
  136. package/renderer/hooks/use-notification-sdk.ts +7 -0
  137. package/renderer/hooks/use-permission-sdk.ts +7 -0
  138. package/renderer/hooks/use-permission.ts +325 -0
  139. package/renderer/hooks/use-platform-sdk.ts +7 -0
  140. package/renderer/hooks/use-plugin-sdk.ts +16 -0
  141. package/renderer/hooks/use-settings-sdk.ts +7 -0
  142. package/renderer/hooks/use-update-sdk.ts +21 -0
  143. package/renderer/index.ts +1 -0
  144. package/renderer/ref.ts +19 -10
  145. package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
  146. package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
  147. package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
  148. package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
  149. package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
  150. package/renderer/shared/components/index.ts +5 -0
  151. package/renderer/shared/components/shims-vue.d.ts +5 -0
  152. package/renderer/shared/index.ts +2 -0
  153. package/renderer/shared/plugin-detail.ts +62 -0
  154. package/renderer/storage/app-settings.ts +3 -1
  155. package/renderer/storage/base-storage.ts +508 -82
  156. package/renderer/storage/intelligence-storage.ts +37 -46
  157. package/renderer/storage/openers.ts +3 -1
  158. package/renderer/storage/storage-subscription.ts +126 -42
  159. package/renderer/touch-sdk/env.ts +10 -10
  160. package/renderer/touch-sdk/index.ts +114 -18
  161. package/renderer/touch-sdk/terminal.ts +24 -13
  162. package/search/feature-matcher.ts +279 -0
  163. package/search/fuzzy-match.ts +64 -34
  164. package/search/index.ts +10 -0
  165. package/search/levenshtein-utils.ts +17 -11
  166. package/transport/errors.ts +310 -0
  167. package/transport/event/builder.ts +378 -0
  168. package/transport/event/index.ts +7 -0
  169. package/transport/event/types.ts +292 -0
  170. package/transport/events/index.ts +2670 -0
  171. package/transport/events/meta-overlay.ts +79 -0
  172. package/transport/events/types/agents.ts +177 -0
  173. package/transport/events/types/app-index.ts +9 -0
  174. package/transport/events/types/app.ts +475 -0
  175. package/transport/events/types/box-item.ts +222 -0
  176. package/transport/events/types/clipboard.ts +80 -0
  177. package/transport/events/types/core-box.ts +534 -0
  178. package/transport/events/types/device-idle.ts +7 -0
  179. package/transport/events/types/division-box.ts +99 -0
  180. package/transport/events/types/download.ts +115 -0
  181. package/transport/events/types/file-index.ts +73 -0
  182. package/transport/events/types/flow.ts +149 -0
  183. package/transport/events/types/index.ts +70 -0
  184. package/transport/events/types/market.ts +39 -0
  185. package/transport/events/types/meta-overlay.ts +184 -0
  186. package/transport/events/types/notification.ts +140 -0
  187. package/transport/events/types/permission.ts +90 -0
  188. package/transport/events/types/platform.ts +8 -0
  189. package/transport/events/types/plugin.ts +620 -0
  190. package/transport/events/types/sentry.ts +20 -0
  191. package/transport/events/types/storage.ts +208 -0
  192. package/transport/events/types/transport.ts +60 -0
  193. package/transport/events/types/tray.ts +16 -0
  194. package/transport/events/types/update.ts +78 -0
  195. package/transport/index.ts +139 -0
  196. package/transport/main.ts +2 -0
  197. package/transport/sdk/constants.ts +29 -0
  198. package/transport/sdk/domains/agents-market.ts +47 -0
  199. package/transport/sdk/domains/agents.ts +62 -0
  200. package/transport/sdk/domains/app.ts +48 -0
  201. package/transport/sdk/domains/disposable.ts +35 -0
  202. package/transport/sdk/domains/download.ts +139 -0
  203. package/transport/sdk/domains/index.ts +13 -0
  204. package/transport/sdk/domains/intelligence.ts +616 -0
  205. package/transport/sdk/domains/market.ts +35 -0
  206. package/transport/sdk/domains/notification.ts +62 -0
  207. package/transport/sdk/domains/permission.ts +85 -0
  208. package/transport/sdk/domains/platform.ts +19 -0
  209. package/transport/sdk/domains/plugin.ts +144 -0
  210. package/transport/sdk/domains/settings.ts +92 -0
  211. package/transport/sdk/domains/update.ts +64 -0
  212. package/transport/sdk/index.ts +60 -0
  213. package/transport/sdk/main-transport.ts +710 -0
  214. package/transport/sdk/main.ts +9 -0
  215. package/transport/sdk/plugin-transport.ts +654 -0
  216. package/transport/sdk/port-policy.ts +38 -0
  217. package/transport/sdk/renderer-transport.ts +1165 -0
  218. package/transport/types.ts +605 -0
  219. package/types/agent.ts +399 -0
  220. package/types/cloud-sync.ts +157 -0
  221. package/types/division-box.ts +47 -27
  222. package/types/download.ts +1 -0
  223. package/types/flow.ts +63 -12
  224. package/types/icon.ts +2 -1
  225. package/types/index.ts +5 -0
  226. package/types/intelligence.ts +1492 -81
  227. package/types/modules/base.ts +2 -0
  228. package/types/path-browserify.d.ts +5 -0
  229. package/types/platform.ts +12 -0
  230. package/types/startup-info.ts +32 -0
  231. package/types/touch-app-core.ts +8 -8
  232. package/types/update.ts +94 -1
  233. package/vitest.config.ts +25 -0
  234. package/auth/useClerkConfig.ts +0 -40
  235. package/auth/useClerkProvider.ts +0 -52
@@ -0,0 +1,710 @@
1
+ /**
2
+ * @fileoverview Main process TuffTransport implementation
3
+ * @module @talex-touch/utils/transport/sdk/main-transport
4
+ */
5
+
6
+ import type { IpcMainInvokeEvent, MessagePortMain, WebContents } from 'electron'
7
+ import type { ITouchChannel } from '../../channel'
8
+ import type { TuffEvent } from '../event/types'
9
+ import type { TransportPortConfirmPayload, TransportPortEnvelope, TransportPortScope, TransportPortUpgradeResponse } from '../events'
10
+ import type {
11
+ HandlerContext,
12
+ ITuffTransportMain,
13
+ PluginKeyManager,
14
+ StreamContext,
15
+ } from '../types'
16
+ import { randomUUID } from 'node:crypto'
17
+ import * as electron from 'electron'
18
+ import { ChannelType, DataCode } from '../../channel'
19
+ import { assertTuffEvent } from '../event/builder'
20
+ import { TransportEvents } from '../events'
21
+ import { STREAM_SUFFIXES } from './constants'
22
+ import { isPortChannelEnabled } from './port-policy'
23
+
24
+ const { ipcMain, MessageChannelMain } = electron
25
+
26
+ type InvokeHandler<TReq, TRes> = (
27
+ payload: TReq,
28
+ event: IpcMainInvokeEvent,
29
+ ) => TRes | Promise<TRes>
30
+
31
+ const invokeHandlers = new Map<string, Set<InvokeHandler<any, any>>>()
32
+ const invokeDisposers = new Map<string, () => void>()
33
+
34
+ function registerInvokeHandler<TReq, TRes>(
35
+ eventName: string,
36
+ handler: InvokeHandler<TReq, TRes>,
37
+ ): () => void {
38
+ let handlers = invokeHandlers.get(eventName)
39
+ if (!handlers) {
40
+ handlers = new Set()
41
+ invokeHandlers.set(eventName, handlers)
42
+
43
+ ipcMain.handle(eventName, async (event, payload) => {
44
+ const active = invokeHandlers.get(eventName)
45
+ if (!active || active.size === 0) {
46
+ return undefined
47
+ }
48
+
49
+ let result: unknown
50
+ for (const fn of active) {
51
+ result = await fn(payload as TReq, event)
52
+ }
53
+ return result as TRes
54
+ })
55
+
56
+ invokeDisposers.set(eventName, () => ipcMain.removeHandler(eventName))
57
+ }
58
+
59
+ handlers.add(handler)
60
+
61
+ return () => {
62
+ const current = invokeHandlers.get(eventName)
63
+ if (!current) {
64
+ return
65
+ }
66
+ current.delete(handler)
67
+ if (current.size === 0) {
68
+ invokeHandlers.delete(eventName)
69
+ invokeDisposers.get(eventName)?.()
70
+ invokeDisposers.delete(eventName)
71
+ }
72
+ }
73
+ }
74
+
75
+ interface PortRecord {
76
+ port: MessagePortMain
77
+ sender: WebContents
78
+ channel: string
79
+ scope: TransportPortScope
80
+ windowId?: number
81
+ plugin?: string
82
+ permissions?: string[]
83
+ confirmed: boolean
84
+ createdAt: number
85
+ confirmTimeout?: NodeJS.Timeout
86
+ }
87
+
88
+ const PORT_CONFIRM_TIMEOUT_MS = 10000
89
+ const portRegistry = new Map<string, PortRecord>()
90
+ const portsBySenderId = new Map<number, Set<string>>()
91
+ const senderCleanupRegistered = new WeakSet<WebContents>()
92
+ let portHandlersRegistered = false
93
+
94
+ interface PortLookup {
95
+ portId: string
96
+ record: PortRecord
97
+ }
98
+
99
+ function resolvePortRecord(channel: string, sender: WebContents, scope?: TransportPortScope): PortLookup | null {
100
+ const portIds = portsBySenderId.get(sender.id)
101
+ if (!portIds)
102
+ return null
103
+
104
+ for (const portId of portIds) {
105
+ const record = portRegistry.get(portId)
106
+ if (!record || !record.confirmed)
107
+ continue
108
+ if (record.channel !== channel)
109
+ continue
110
+ if (scope && record.scope !== scope)
111
+ continue
112
+ if (record.scope === 'window' && record.windowId !== undefined && record.windowId !== sender.id) {
113
+ continue
114
+ }
115
+ return { portId, record }
116
+ }
117
+ return null
118
+ }
119
+
120
+ function postPortMessage(lookup: PortLookup, message: TransportPortEnvelope): boolean {
121
+ try {
122
+ lookup.record.port.postMessage(message)
123
+ return true
124
+ }
125
+ catch (error) {
126
+ const errorMessage = error instanceof Error ? error.message : String(error)
127
+ console.warn(`[TuffTransport] Port send failed for \"${message.channel}\": ${errorMessage}`)
128
+ return false
129
+ }
130
+ }
131
+
132
+ function registerPortHandlers(transport: TuffMainTransport): void {
133
+ if (portHandlersRegistered) {
134
+ return
135
+ }
136
+ portHandlersRegistered = true
137
+
138
+ const buildError = (code: string, message: string) => ({ code, message })
139
+
140
+ const removePort = (portId: string, reason?: string) => {
141
+ const record = portRegistry.get(portId)
142
+ if (!record)
143
+ return
144
+
145
+ if (record.confirmTimeout) {
146
+ clearTimeout(record.confirmTimeout)
147
+ }
148
+ try {
149
+ record.port.close()
150
+ }
151
+ catch {}
152
+
153
+ portRegistry.delete(portId)
154
+ const senderPorts = portsBySenderId.get(record.sender.id)
155
+ if (senderPorts) {
156
+ senderPorts.delete(portId)
157
+ if (senderPorts.size === 0) {
158
+ portsBySenderId.delete(record.sender.id)
159
+ }
160
+ }
161
+
162
+ if (reason) {
163
+ console.warn(`[TuffTransport] Port ${portId} closed: ${reason}`)
164
+ }
165
+ }
166
+
167
+ const ensureSenderCleanup = (sender: WebContents) => {
168
+ if (senderCleanupRegistered.has(sender))
169
+ return
170
+ senderCleanupRegistered.add(sender)
171
+ sender.once('destroyed', () => {
172
+ const portIds = portsBySenderId.get(sender.id)
173
+ if (!portIds)
174
+ return
175
+ for (const portId of portIds) {
176
+ removePort(portId, 'sender_destroyed')
177
+ }
178
+ portsBySenderId.delete(sender.id)
179
+ })
180
+ }
181
+
182
+ const resolveScope = (scope?: TransportPortScope): TransportPortScope | null => {
183
+ if (!scope)
184
+ return 'window'
185
+ if (scope === 'app' || scope === 'window' || scope === 'plugin')
186
+ return scope
187
+ return null
188
+ }
189
+
190
+ transport.on(TransportEvents.port.upgrade, async (payload, context) => {
191
+ const sender = context.sender as WebContents | undefined
192
+ if (!sender || typeof sender.postMessage !== 'function') {
193
+ return {
194
+ accepted: false,
195
+ channel: payload?.channel ?? '',
196
+ error: buildError('sender_unavailable', 'Sender webContents is unavailable'),
197
+ } satisfies TransportPortUpgradeResponse
198
+ }
199
+
200
+ if (!MessageChannelMain) {
201
+ return {
202
+ accepted: false,
203
+ channel: payload?.channel ?? '',
204
+ error: buildError('not_supported', 'MessageChannelMain is not available'),
205
+ } satisfies TransportPortUpgradeResponse
206
+ }
207
+
208
+ const channel = payload?.channel?.trim()
209
+ if (!channel) {
210
+ return {
211
+ accepted: false,
212
+ channel: '',
213
+ error: buildError('invalid_request', 'Channel is required'),
214
+ } satisfies TransportPortUpgradeResponse
215
+ }
216
+
217
+ const scope = resolveScope(payload?.scope)
218
+ if (!scope) {
219
+ return {
220
+ accepted: false,
221
+ channel,
222
+ error: buildError('invalid_scope', 'Scope is invalid'),
223
+ } satisfies TransportPortUpgradeResponse
224
+ }
225
+
226
+ const windowId = payload?.windowId ?? sender.id
227
+ if (scope === 'window' && windowId !== sender.id) {
228
+ return {
229
+ accepted: false,
230
+ channel,
231
+ scope,
232
+ error: buildError('window_mismatch', 'Window id does not match sender'),
233
+ } satisfies TransportPortUpgradeResponse
234
+ }
235
+
236
+ const plugin = payload?.plugin ?? context.plugin?.name
237
+ if (scope === 'plugin' && !plugin) {
238
+ return {
239
+ accepted: false,
240
+ channel,
241
+ scope,
242
+ error: buildError('plugin_required', 'Plugin name is required for plugin scope'),
243
+ } satisfies TransportPortUpgradeResponse
244
+ }
245
+
246
+ if (context.plugin?.name && plugin && plugin !== context.plugin.name) {
247
+ return {
248
+ accepted: false,
249
+ channel,
250
+ scope,
251
+ error: buildError('plugin_mismatch', 'Plugin name does not match sender'),
252
+ } satisfies TransportPortUpgradeResponse
253
+ }
254
+
255
+ const { port1, port2 } = new MessageChannelMain()
256
+ const portId = randomUUID()
257
+ const record: PortRecord = {
258
+ port: port2,
259
+ sender,
260
+ channel,
261
+ scope,
262
+ windowId,
263
+ plugin: plugin || undefined,
264
+ permissions: payload?.permissions,
265
+ confirmed: false,
266
+ createdAt: Date.now(),
267
+ }
268
+
269
+ portRegistry.set(portId, record)
270
+ const senderPorts = portsBySenderId.get(sender.id) ?? new Set<string>()
271
+ senderPorts.add(portId)
272
+ portsBySenderId.set(sender.id, senderPorts)
273
+ ensureSenderCleanup(sender)
274
+
275
+ port2.on('close', () => {
276
+ removePort(portId, 'port_closed')
277
+ })
278
+ port2.start()
279
+
280
+ const confirmPayload: TransportPortConfirmPayload = {
281
+ channel,
282
+ portId,
283
+ scope,
284
+ permissions: payload?.permissions,
285
+ }
286
+
287
+ try {
288
+ sender.postMessage(TransportEvents.port.confirm.toEventName(), confirmPayload, [port1])
289
+ }
290
+ catch (error) {
291
+ const message = error instanceof Error ? error.message : String(error)
292
+ removePort(portId, `postMessage_failed:${message}`)
293
+ return {
294
+ accepted: false,
295
+ channel,
296
+ scope,
297
+ error: buildError('post_message_failed', message),
298
+ } satisfies TransportPortUpgradeResponse
299
+ }
300
+
301
+ record.confirmTimeout = setTimeout(() => {
302
+ if (!record.confirmed) {
303
+ removePort(portId, 'confirm_timeout')
304
+ }
305
+ }, PORT_CONFIRM_TIMEOUT_MS)
306
+
307
+ return {
308
+ accepted: true,
309
+ channel,
310
+ scope,
311
+ permissions: payload?.permissions,
312
+ portId,
313
+ } satisfies TransportPortUpgradeResponse
314
+ })
315
+
316
+ transport.on(TransportEvents.port.confirm, (payload, context) => {
317
+ const portId = payload?.portId
318
+ if (!portId)
319
+ return
320
+ const record = portRegistry.get(portId)
321
+ if (!record)
322
+ return
323
+ if (context.sender && record.sender.id !== (context.sender as WebContents).id) {
324
+ return
325
+ }
326
+ record.confirmed = true
327
+ if (record.confirmTimeout) {
328
+ clearTimeout(record.confirmTimeout)
329
+ record.confirmTimeout = undefined
330
+ }
331
+ })
332
+
333
+ transport.on(TransportEvents.port.close, (payload, context) => {
334
+ const sender = context.sender as WebContents | undefined
335
+ const portId = payload?.portId
336
+ if (portId) {
337
+ if (!sender || portRegistry.get(portId)?.sender.id === sender.id) {
338
+ removePort(portId, payload?.reason ?? 'closed')
339
+ }
340
+ return
341
+ }
342
+
343
+ if (!sender)
344
+ return
345
+ const portIds = portsBySenderId.get(sender.id)
346
+ if (!portIds)
347
+ return
348
+ for (const id of portIds) {
349
+ const record = portRegistry.get(id)
350
+ if (!record)
351
+ continue
352
+ if (payload?.channel && record.channel !== payload.channel)
353
+ continue
354
+ removePort(id, payload?.reason ?? 'closed')
355
+ }
356
+ })
357
+
358
+ transport.on(TransportEvents.port.error, (payload, context) => {
359
+ const portId = payload?.portId
360
+ if (payload?.error) {
361
+ console.warn('[TuffTransport] Port error:', payload.error)
362
+ }
363
+ if (!portId)
364
+ return
365
+ const sender = context.sender as WebContents | undefined
366
+ if (!sender || portRegistry.get(portId)?.sender.id === sender.id) {
367
+ removePort(portId, 'error')
368
+ }
369
+ })
370
+ }
371
+
372
+ /**
373
+ * Main process transport implementation.
374
+ * Adapts the legacy TouchChannel to the new TuffTransportMain interface.
375
+ */
376
+ export class TuffMainTransport implements ITuffTransportMain {
377
+ constructor(
378
+ private channel: ITouchChannel,
379
+ public readonly keyManager: PluginKeyManager,
380
+ ) {
381
+ registerPortHandlers(this)
382
+ }
383
+
384
+ /**
385
+ * Registers an event handler.
386
+ */
387
+ on<TReq, TRes>(
388
+ event: TuffEvent<TReq, TRes>,
389
+ handler: (payload: TReq, context: HandlerContext) => TRes | Promise<TRes>,
390
+ ): () => void {
391
+ assertTuffEvent(event, 'TuffMainTransport.on')
392
+
393
+ const eventName = event.toEventName()
394
+
395
+ const baseHandler = async (payload: TReq, context: HandlerContext) => {
396
+ try {
397
+ return await handler(payload, context)
398
+ }
399
+ catch (error) {
400
+ const errorMessage = error instanceof Error ? error.message : String(error)
401
+ console.error(`[TuffTransport] Handler error for \"${eventName}\":`, errorMessage)
402
+ throw error
403
+ }
404
+ }
405
+
406
+ const channelHandler = async (data: any) => {
407
+ const context: HandlerContext = {
408
+ sender: data.header?.event?.sender as any,
409
+ eventName,
410
+ plugin: data.plugin
411
+ ? {
412
+ name: data.plugin,
413
+ uniqueKey: data.header?.uniqueKey || '',
414
+ verified: Boolean(data.header?.uniqueKey),
415
+ }
416
+ : undefined,
417
+ }
418
+
419
+ return baseHandler(data.data as TReq, context)
420
+ }
421
+
422
+ const invokeHandler: InvokeHandler<TReq, TRes> = (payload, event) => {
423
+ const context: HandlerContext = {
424
+ sender: event.sender as any,
425
+ eventName,
426
+ }
427
+ return baseHandler(payload, context)
428
+ }
429
+
430
+ const unregisterMain = this.channel.regChannel(ChannelType.MAIN, eventName, channelHandler)
431
+ const unregisterPlugin = this.channel.regChannel(ChannelType.PLUGIN, eventName, channelHandler)
432
+ const unregisterInvoke = registerInvokeHandler(eventName, invokeHandler)
433
+
434
+ return () => {
435
+ unregisterMain()
436
+ unregisterPlugin()
437
+ unregisterInvoke()
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Registers a stream handler.
443
+ *
444
+ * @remarks
445
+ * Phase 1 implementation uses IPC events to simulate streaming.
446
+ */
447
+ onStream<TReq, TChunk>(
448
+ event: TuffEvent<TReq, AsyncIterable<TChunk>>,
449
+ handler: (payload: TReq, context: StreamContext<TChunk>) => void | Promise<void>,
450
+ ): () => void {
451
+ assertTuffEvent(event, 'TuffMainTransport.onStream')
452
+
453
+ const eventName = event.toEventName()
454
+ const portEnabled = isPortChannelEnabled(eventName)
455
+ const startEventName = `${eventName}${STREAM_SUFFIXES.START}`
456
+ const cancelEventName = `${eventName}${STREAM_SUFFIXES.CANCEL}`
457
+
458
+ const streams = new Map<string, { cancelled: boolean }>()
459
+
460
+ const startHandler = (data: any) => {
461
+ const rawPayload = data?.data as { streamId?: string, [key: string]: any } | undefined
462
+ const streamId = rawPayload?.streamId
463
+ const sender = data?.header?.event?.sender as any
464
+
465
+ if (!streamId || !sender) {
466
+ throw new Error(`[TuffTransport] Invalid stream start for "${eventName}"`)
467
+ }
468
+
469
+ streams.set(streamId, { cancelled: false })
470
+
471
+ const portLookup = portEnabled
472
+ ? resolvePortRecord(eventName, sender as WebContents, 'window')
473
+ : null
474
+
475
+ const sendPortStreamMessage = (message: TransportPortEnvelope): boolean => {
476
+ if (!portLookup)
477
+ return false
478
+ const record = portRegistry.get(portLookup.portId)
479
+ if (!record || !record.confirmed)
480
+ return false
481
+ return postPortMessage({ portId: portLookup.portId, record }, message)
482
+ }
483
+
484
+ const sendToSender = (name: string, payload: any) => {
485
+ try {
486
+ sender.send('@main-process-message', {
487
+ code: DataCode.SUCCESS,
488
+ data: payload,
489
+ name,
490
+ header: { status: 'request', type: ChannelType.MAIN },
491
+ })
492
+ }
493
+ catch {
494
+ // Ignore send failures (renderer may have been destroyed)
495
+ }
496
+ }
497
+
498
+ const cleanup = () => {
499
+ streams.delete(streamId)
500
+ }
501
+
502
+ const streamContext: StreamContext<TChunk> = {
503
+ emit: (chunk: TChunk) => {
504
+ if (streams.get(streamId)?.cancelled)
505
+ return
506
+ const portSent = sendPortStreamMessage({
507
+ channel: eventName,
508
+ portId: portLookup?.portId,
509
+ streamId,
510
+ type: 'data',
511
+ payload: { chunk },
512
+ })
513
+ if (!portSent) {
514
+ sendToSender(`${eventName}${STREAM_SUFFIXES.DATA}:${streamId}`, { chunk })
515
+ }
516
+ },
517
+ error: (err: Error) => {
518
+ if (streams.get(streamId)?.cancelled)
519
+ return
520
+ const errorMessage = err instanceof Error ? err.message : String(err)
521
+ const portSent = sendPortStreamMessage({
522
+ channel: eventName,
523
+ portId: portLookup?.portId,
524
+ streamId,
525
+ type: 'error',
526
+ payload: { error: errorMessage },
527
+ error: { code: 'stream_error', message: errorMessage },
528
+ })
529
+ if (!portSent) {
530
+ sendToSender(`${eventName}${STREAM_SUFFIXES.ERROR}:${streamId}`, {
531
+ error: errorMessage,
532
+ })
533
+ }
534
+ cleanup()
535
+ },
536
+ end: () => {
537
+ if (streams.get(streamId)?.cancelled)
538
+ return
539
+ const portSent = sendPortStreamMessage({
540
+ channel: eventName,
541
+ portId: portLookup?.portId,
542
+ streamId,
543
+ type: 'close',
544
+ })
545
+ if (!portSent) {
546
+ sendToSender(`${eventName}${STREAM_SUFFIXES.END}:${streamId}`, {})
547
+ }
548
+ cleanup()
549
+ },
550
+ isCancelled: () => {
551
+ return streams.get(streamId)?.cancelled === true
552
+ },
553
+ streamId,
554
+ }
555
+
556
+ const payload = rawPayload ? { ...rawPayload } : {}
557
+ delete (payload as any).streamId
558
+ const requestPayload = payload as unknown as TReq
559
+
560
+ Promise.resolve(handler(requestPayload, streamContext)).catch((error) => {
561
+ const errorMessage = error instanceof Error ? error.message : String(error)
562
+ console.error(`[TuffTransport] Stream handler error for "${eventName}":`, errorMessage)
563
+ streamContext.error(error as Error)
564
+ })
565
+ }
566
+
567
+ const cancelHandler = (data: any) => {
568
+ const rawPayload = data?.data as { streamId?: string } | undefined
569
+ const streamId = rawPayload?.streamId
570
+ if (!streamId)
571
+ return
572
+ const state = streams.get(streamId)
573
+ if (state)
574
+ state.cancelled = true
575
+ streams.delete(streamId)
576
+ }
577
+
578
+ const startCleanupMain = this.channel.regChannel(ChannelType.MAIN, startEventName, startHandler)
579
+ const cancelCleanupMain = this.channel.regChannel(ChannelType.MAIN, cancelEventName, cancelHandler)
580
+ const startCleanupPlugin = this.channel.regChannel(ChannelType.PLUGIN, startEventName, startHandler)
581
+ const cancelCleanupPlugin = this.channel.regChannel(ChannelType.PLUGIN, cancelEventName, cancelHandler)
582
+
583
+ return () => {
584
+ startCleanupMain()
585
+ cancelCleanupMain()
586
+ startCleanupPlugin()
587
+ cancelCleanupPlugin()
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Sends a message to a specific window.
593
+ */
594
+ async sendToWindow<TReq, TRes>(
595
+ windowId: number,
596
+ event: TuffEvent<TReq, TRes>,
597
+ payload: TReq,
598
+ ): Promise<TRes> {
599
+ assertTuffEvent(event, 'TuffMainTransport.sendToWindow')
600
+
601
+ const eventName = event.toEventName()
602
+ const { BrowserWindow } = await import('electron')
603
+ const win = BrowserWindow.fromId(windowId)
604
+ if (!win) {
605
+ throw new Error(`[TuffTransport] Cannot find BrowserWindow for id=${windowId}`)
606
+ }
607
+ if (isPortChannelEnabled(eventName)) {
608
+ const portLookup = resolvePortRecord(eventName, win.webContents, 'window')
609
+ if (portLookup) {
610
+ const portSent = postPortMessage(portLookup, {
611
+ channel: eventName,
612
+ portId: portLookup.portId,
613
+ type: 'data',
614
+ payload,
615
+ })
616
+ if (portSent) {
617
+ return undefined as TRes
618
+ }
619
+ }
620
+ }
621
+ return this.channel.sendTo(win, ChannelType.MAIN, eventName, payload)
622
+ }
623
+
624
+ /**
625
+ * Broadcasts a message to a specific window (fire-and-forget).
626
+ */
627
+ broadcastToWindow<TReq>(
628
+ windowId: number,
629
+ event: TuffEvent<TReq, void>,
630
+ payload: TReq,
631
+ ): void {
632
+ assertTuffEvent(event, 'TuffMainTransport.broadcastToWindow')
633
+
634
+ const eventName = event.toEventName()
635
+ const win = electron.BrowserWindow.fromId(windowId)
636
+ if (!win) {
637
+ throw new Error(`[TuffTransport] Cannot find BrowserWindow for id=${windowId}`)
638
+ }
639
+ this.channel.broadcastTo(win, ChannelType.MAIN, eventName, payload)
640
+ }
641
+
642
+ /**
643
+ * Sends a message to a specific WebContents.
644
+ */
645
+ async sendTo<TReq, TRes>(
646
+ webContents: any,
647
+ event: TuffEvent<TReq, TRes>,
648
+ payload: TReq,
649
+ ): Promise<TRes> {
650
+ assertTuffEvent(event, 'TuffMainTransport.sendTo')
651
+
652
+ const eventName = event.toEventName()
653
+
654
+ // Find the BrowserWindow that owns this WebContents
655
+ const { BrowserWindow } = await import('electron')
656
+ const windows = BrowserWindow.getAllWindows()
657
+ const targetWindow = windows.find(win => win.webContents === webContents)
658
+
659
+ if (!targetWindow) {
660
+ throw new Error(
661
+ '[TuffTransport] Cannot find BrowserWindow for WebContents. '
662
+ + 'Make sure the WebContents belongs to an existing BrowserWindow.',
663
+ )
664
+ }
665
+
666
+ if (isPortChannelEnabled(eventName)) {
667
+ const portLookup = resolvePortRecord(eventName, targetWindow.webContents, 'window')
668
+ if (portLookup) {
669
+ const portSent = postPortMessage(portLookup, {
670
+ channel: eventName,
671
+ portId: portLookup.portId,
672
+ type: 'data',
673
+ payload,
674
+ })
675
+ if (portSent) {
676
+ return undefined as TRes
677
+ }
678
+ }
679
+ }
680
+
681
+ return this.channel.sendTo(targetWindow, ChannelType.MAIN, eventName, payload)
682
+ }
683
+
684
+ /**
685
+ * Sends a message to a plugin's renderer.
686
+ */
687
+ async sendToPlugin<TReq, TRes>(
688
+ pluginName: string,
689
+ event: TuffEvent<TReq, TRes>,
690
+ payload: TReq,
691
+ ): Promise<TRes> {
692
+ assertTuffEvent(event, 'TuffMainTransport.sendToPlugin')
693
+
694
+ const eventName = event.toEventName()
695
+ return this.channel.sendPlugin(pluginName, eventName, payload)
696
+ }
697
+
698
+ /**
699
+ * Broadcasts a message to all windows.
700
+ */
701
+ broadcast<TReq>(
702
+ event: TuffEvent<TReq, void>,
703
+ payload: TReq,
704
+ ): void {
705
+ assertTuffEvent(event, 'TuffMainTransport.broadcast')
706
+
707
+ const eventName = event.toEventName()
708
+ this.channel.broadcast(ChannelType.MAIN, eventName, payload)
709
+ }
710
+ }
@@ -0,0 +1,9 @@
1
+ import type { ITuffTransportMain } from '../types'
2
+ import { TuffMainTransport } from './main-transport'
3
+
4
+ export function getTuffTransportMain(
5
+ channel: any,
6
+ keyManager: any,
7
+ ): ITuffTransportMain {
8
+ return new TuffMainTransport(channel, keyManager)
9
+ }