@talex-touch/utils 1.0.42 → 1.0.45
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/.eslintcache +1 -0
- package/__tests__/cloud-sync-sdk.test.ts +442 -0
- package/__tests__/icons/icons.test.ts +84 -0
- package/__tests__/plugin-sdk-lifecycle.test.ts +130 -0
- package/__tests__/power-sdk.test.ts +143 -0
- package/__tests__/preset-export-types.test.ts +108 -0
- package/__tests__/search/fuzzy-match.test.ts +137 -0
- package/__tests__/transport/port-policy.test.ts +44 -0
- package/__tests__/transport-domain-sdks.test.ts +152 -0
- package/__tests__/types/update.test.ts +67 -0
- package/account/account-sdk.ts +915 -0
- package/account/index.ts +2 -0
- package/account/types.ts +321 -0
- package/analytics/client.ts +136 -0
- package/analytics/index.ts +2 -0
- package/analytics/types.ts +156 -0
- package/animation/auto-resize.ts +322 -0
- package/animation/window-node.ts +26 -19
- package/auth/clerk-types.ts +12 -30
- package/auth/index.ts +0 -2
- package/auth/useAuthState.ts +6 -14
- package/base/index.ts +2 -0
- package/base/log-level.ts +105 -0
- package/channel/index.ts +170 -69
- package/cloud-sync/cloud-sync-sdk.ts +450 -0
- package/cloud-sync/index.ts +1 -0
- package/common/file-scan-utils.ts +17 -9
- package/common/index.ts +4 -0
- package/common/logger/index.ts +46 -0
- package/common/logger/logger-manager.ts +303 -0
- package/common/logger/module-logger.ts +270 -0
- package/common/logger/transport-logger.ts +234 -0
- package/common/logger/types.ts +93 -0
- package/common/search/gather.ts +48 -6
- package/common/search/index.ts +8 -0
- package/common/storage/constants.ts +13 -0
- package/common/storage/entity/app-settings.ts +245 -0
- package/common/storage/entity/index.ts +3 -0
- package/common/storage/entity/layout-atom-types.ts +147 -0
- package/common/storage/entity/openers.ts +1 -0
- package/common/storage/entity/preset-cloud-api.ts +132 -0
- package/common/storage/entity/preset-export-types.ts +256 -0
- package/common/storage/entity/shortcut-settings.ts +1 -0
- package/common/storage/shortcut-storage.ts +11 -0
- package/common/utils/clone-diagnostics.ts +105 -0
- package/common/utils/file.ts +16 -8
- package/common/utils/index.ts +6 -2
- package/common/utils/payload-preview.ts +173 -0
- package/common/utils/polling.ts +167 -13
- package/common/utils/safe-path.ts +103 -0
- package/common/utils/safe-shell.ts +115 -0
- package/common/utils/task-queue.ts +4 -1
- package/core-box/builder/tuff-builder.ts +0 -1
- package/core-box/index.ts +1 -1
- package/core-box/recommendation.ts +38 -1
- package/core-box/tuff/tuff-dsl.ts +32 -0
- package/electron/download-manager.ts +10 -7
- package/electron/env-tool.ts +42 -40
- package/electron/index.ts +0 -1
- package/env/index.ts +156 -0
- package/eslint.config.js +55 -0
- package/i18n/index.ts +62 -0
- package/i18n/locales/en.json +226 -0
- package/i18n/locales/zh.json +226 -0
- package/i18n/message-keys.ts +236 -0
- package/i18n/resolver.ts +181 -0
- package/icons/index.ts +257 -0
- package/icons/svg.ts +69 -0
- package/index.ts +9 -1
- package/intelligence/client.ts +72 -42
- package/market/constants.ts +9 -5
- package/market/index.ts +1 -1
- package/market/types.ts +19 -4
- package/package.json +15 -5
- package/permission/index.ts +143 -46
- package/permission/legacy.ts +26 -0
- package/permission/registry.ts +304 -0
- package/permission/types.ts +164 -0
- package/plugin/channel.ts +68 -39
- package/plugin/index.ts +80 -7
- package/plugin/install.ts +3 -0
- package/plugin/log/types.ts +22 -5
- package/plugin/node/logger-manager.ts +11 -3
- package/plugin/node/logger.ts +24 -17
- package/plugin/preload.ts +25 -2
- package/plugin/providers/index.ts +4 -4
- package/plugin/providers/market-client.ts +6 -3
- package/plugin/providers/npm-provider.ts +22 -7
- package/plugin/providers/tpex-provider.ts +22 -8
- package/plugin/sdk/box-items.ts +14 -0
- package/plugin/sdk/box-sdk.ts +64 -0
- package/plugin/sdk/channel.ts +119 -4
- package/plugin/sdk/clipboard.ts +26 -12
- package/plugin/sdk/cloud-sync.ts +113 -0
- package/plugin/sdk/common.ts +19 -11
- package/plugin/sdk/core-box.ts +6 -15
- package/plugin/sdk/division-box.ts +160 -65
- package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
- package/plugin/sdk/feature-sdk.ts +111 -76
- package/plugin/sdk/flow.ts +146 -45
- package/plugin/sdk/hooks/bridge.ts +13 -6
- package/plugin/sdk/hooks/life-cycle.ts +35 -16
- package/plugin/sdk/index.ts +14 -3
- package/plugin/sdk/intelligence.ts +87 -0
- package/plugin/sdk/meta/README.md +179 -0
- package/plugin/sdk/meta-sdk.ts +244 -0
- package/plugin/sdk/notification.ts +9 -0
- package/plugin/sdk/plugin-info.ts +64 -0
- package/plugin/sdk/power.ts +155 -0
- package/plugin/sdk/recommend.ts +21 -0
- package/plugin/sdk/service/index.ts +12 -8
- package/plugin/sdk/sqlite.ts +141 -0
- package/plugin/sdk/storage.ts +2 -6
- package/plugin/sdk/system.ts +2 -9
- package/plugin/sdk/temp-files.ts +41 -0
- package/plugin/sdk/touch-sdk.ts +18 -0
- package/plugin/sdk/types.ts +44 -4
- package/plugin/sdk/window/index.ts +12 -9
- package/plugin/sdk-version.ts +231 -0
- package/preload/renderer.ts +3 -2
- package/renderer/hooks/arg-mapper.ts +16 -2
- package/renderer/hooks/index.ts +13 -0
- package/renderer/hooks/initialize.ts +2 -1
- package/renderer/hooks/use-agent-market-sdk.ts +7 -0
- package/renderer/hooks/use-agent-market.ts +106 -0
- package/renderer/hooks/use-agents-sdk.ts +7 -0
- package/renderer/hooks/use-app-sdk.ts +7 -0
- package/renderer/hooks/use-channel.ts +33 -4
- package/renderer/hooks/use-download-sdk.ts +21 -0
- package/renderer/hooks/use-intelligence-sdk.ts +7 -0
- package/renderer/hooks/use-intelligence-stats.ts +290 -0
- package/renderer/hooks/use-intelligence.ts +55 -214
- package/renderer/hooks/use-market-sdk.ts +16 -0
- package/renderer/hooks/use-notification-sdk.ts +7 -0
- package/renderer/hooks/use-permission-sdk.ts +7 -0
- package/renderer/hooks/use-permission.ts +325 -0
- package/renderer/hooks/use-platform-sdk.ts +7 -0
- package/renderer/hooks/use-plugin-sdk.ts +16 -0
- package/renderer/hooks/use-settings-sdk.ts +7 -0
- package/renderer/hooks/use-update-sdk.ts +21 -0
- package/renderer/index.ts +1 -0
- package/renderer/ref.ts +19 -10
- package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
- package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
- package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
- package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
- package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
- package/renderer/shared/components/index.ts +5 -0
- package/renderer/shared/components/shims-vue.d.ts +5 -0
- package/renderer/shared/index.ts +2 -0
- package/renderer/shared/plugin-detail.ts +62 -0
- package/renderer/storage/app-settings.ts +3 -1
- package/renderer/storage/base-storage.ts +508 -82
- package/renderer/storage/intelligence-storage.ts +31 -40
- package/renderer/storage/openers.ts +3 -1
- package/renderer/storage/storage-subscription.ts +126 -42
- package/renderer/touch-sdk/env.ts +10 -10
- package/renderer/touch-sdk/index.ts +114 -18
- package/renderer/touch-sdk/terminal.ts +24 -13
- package/search/feature-matcher.ts +279 -0
- package/search/fuzzy-match.ts +64 -34
- package/search/index.ts +10 -0
- package/search/levenshtein-utils.ts +17 -11
- package/transport/errors.ts +310 -0
- package/transport/event/builder.ts +378 -0
- package/transport/event/index.ts +7 -0
- package/transport/event/types.ts +292 -0
- package/transport/events/index.ts +2690 -0
- package/transport/events/meta-overlay.ts +79 -0
- package/transport/events/types/agents.ts +177 -0
- package/transport/events/types/app-index.ts +20 -0
- package/transport/events/types/app.ts +475 -0
- package/transport/events/types/box-item.ts +222 -0
- package/transport/events/types/clipboard.ts +80 -0
- package/transport/events/types/core-box.ts +534 -0
- package/transport/events/types/device-idle.ts +7 -0
- package/transport/events/types/division-box.ts +99 -0
- package/transport/events/types/download.ts +115 -0
- package/transport/events/types/file-index.ts +84 -0
- package/transport/events/types/flow.ts +149 -0
- package/transport/events/types/index.ts +70 -0
- package/transport/events/types/market.ts +39 -0
- package/transport/events/types/meta-overlay.ts +184 -0
- package/transport/events/types/notification.ts +140 -0
- package/transport/events/types/permission.ts +90 -0
- package/transport/events/types/platform.ts +8 -0
- package/transport/events/types/plugin.ts +631 -0
- package/transport/events/types/sentry.ts +20 -0
- package/transport/events/types/storage.ts +208 -0
- package/transport/events/types/transport.ts +60 -0
- package/transport/events/types/tray.ts +16 -0
- package/transport/events/types/update.ts +78 -0
- package/transport/index.ts +141 -0
- package/transport/main.ts +2 -0
- package/transport/prelude.ts +208 -0
- package/transport/sdk/constants.ts +29 -0
- package/transport/sdk/domains/agents-market.ts +47 -0
- package/transport/sdk/domains/agents.ts +62 -0
- package/transport/sdk/domains/app.ts +48 -0
- package/transport/sdk/domains/disposable.ts +35 -0
- package/transport/sdk/domains/download.ts +139 -0
- package/transport/sdk/domains/index.ts +13 -0
- package/transport/sdk/domains/intelligence.ts +616 -0
- package/transport/sdk/domains/market.ts +35 -0
- package/transport/sdk/domains/notification.ts +62 -0
- package/transport/sdk/domains/permission.ts +85 -0
- package/transport/sdk/domains/platform.ts +19 -0
- package/transport/sdk/domains/plugin.ts +144 -0
- package/transport/sdk/domains/settings.ts +102 -0
- package/transport/sdk/domains/update.ts +64 -0
- package/transport/sdk/index.ts +60 -0
- package/transport/sdk/main-transport.ts +710 -0
- package/transport/sdk/main.ts +9 -0
- package/transport/sdk/plugin-transport.ts +654 -0
- package/transport/sdk/port-policy.ts +38 -0
- package/transport/sdk/renderer-transport.ts +1165 -0
- package/transport/types.ts +605 -0
- package/types/agent.ts +399 -0
- package/types/cloud-sync.ts +157 -0
- package/types/division-box.ts +31 -31
- package/types/download.ts +1 -0
- package/types/flow.ts +63 -12
- package/types/icon.ts +2 -1
- package/types/index.ts +5 -0
- package/types/intelligence.ts +166 -173
- package/types/modules/base.ts +2 -0
- package/types/path-browserify.d.ts +5 -0
- package/types/platform.ts +12 -0
- package/types/startup-info.ts +32 -0
- package/types/touch-app-core.ts +8 -8
- package/types/update.ts +94 -1
- package/vitest.config.ts +25 -0
- package/auth/useClerkConfig.ts +0 -40
- 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
|
+
}
|