@talex-touch/utils 1.0.42 → 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.
- 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 +2670 -0
- package/transport/events/meta-overlay.ts +79 -0
- package/transport/events/types/agents.ts +177 -0
- package/transport/events/types/app-index.ts +9 -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 +73 -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 +620 -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 +139 -0
- package/transport/main.ts +2 -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 +92 -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,654 @@
|
|
|
1
|
+
import type { TuffEvent } from '../event/types'
|
|
2
|
+
import type {
|
|
3
|
+
TransportPortConfirmPayload,
|
|
4
|
+
TransportPortEnvelope,
|
|
5
|
+
TransportPortUpgradeRequest,
|
|
6
|
+
TransportPortUpgradeResponse,
|
|
7
|
+
} from '../events'
|
|
8
|
+
import type {
|
|
9
|
+
ITuffTransport,
|
|
10
|
+
SendOptions,
|
|
11
|
+
StreamController,
|
|
12
|
+
StreamOptions,
|
|
13
|
+
TransportPortHandle,
|
|
14
|
+
TransportPortOpenOptions,
|
|
15
|
+
} from '../types'
|
|
16
|
+
import { assertTuffEvent } from '../event/builder'
|
|
17
|
+
import { TransportEvents } from '../events'
|
|
18
|
+
import { isPortChannelEnabled } from './port-policy'
|
|
19
|
+
|
|
20
|
+
interface IpcRendererLike {
|
|
21
|
+
on?: (channel: string, listener: (event: any, ...args: any[]) => void) => void
|
|
22
|
+
removeListener?: (channel: string, listener: (event: any, ...args: any[]) => void) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PortConfirmRecord {
|
|
26
|
+
port: MessagePort
|
|
27
|
+
payload: TransportPortConfirmPayload
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PORT_CONFIRM_TIMEOUT_MS = 10000
|
|
31
|
+
|
|
32
|
+
function resolveIpcRenderer(): IpcRendererLike | null {
|
|
33
|
+
if (typeof globalThis === 'undefined') {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const g = globalThis as any
|
|
38
|
+
const electron = g.electron ?? g.window?.electron
|
|
39
|
+
if (electron?.ipcRenderer) {
|
|
40
|
+
return electron.ipcRenderer as IpcRendererLike
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const requireFn = typeof g.require === 'function'
|
|
44
|
+
? g.require
|
|
45
|
+
: typeof require === 'function'
|
|
46
|
+
? require
|
|
47
|
+
: null
|
|
48
|
+
if (!requireFn) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const electronModule = requireFn('electron')
|
|
54
|
+
return electronModule?.ipcRenderer as IpcRendererLike
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolvePluginName(): string | undefined {
|
|
62
|
+
const plugin = (globalThis as any)?.$plugin
|
|
63
|
+
const name = plugin?.name
|
|
64
|
+
return typeof name === 'string' && name.trim().length > 0 ? name.trim() : undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface CacheEntry {
|
|
68
|
+
value: unknown
|
|
69
|
+
expiresAt?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface PortEventSubscription {
|
|
73
|
+
refCount: number
|
|
74
|
+
handle: TransportPortHandle | null
|
|
75
|
+
cleanup: (() => void) | null
|
|
76
|
+
opening: Promise<TransportPortHandle | null> | null
|
|
77
|
+
closing: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface CacheConfig {
|
|
81
|
+
key?: string
|
|
82
|
+
mode: 'prefer' | 'only'
|
|
83
|
+
ttlMs?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeCacheOptions(options?: SendOptions): CacheConfig | null {
|
|
87
|
+
if (!options?.cache) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.cache === true) {
|
|
92
|
+
return { mode: 'prefer' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const mode = options.cache.mode ?? 'prefer'
|
|
96
|
+
return {
|
|
97
|
+
key: options.cache.key,
|
|
98
|
+
mode,
|
|
99
|
+
ttlMs: options.cache.ttlMs,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildCacheKey(eventName: string, payload: unknown, overrideKey?: string): string {
|
|
104
|
+
if (overrideKey) {
|
|
105
|
+
return overrideKey
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (payload === undefined) {
|
|
109
|
+
return `${eventName}:__void__`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
return `${eventName}:${JSON.stringify(payload)}`
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return `${eventName}:${Object.prototype.toString.call(payload)}`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface PluginChannelLike {
|
|
121
|
+
send?: (eventName: string, payload?: any) => Promise<any>
|
|
122
|
+
sendToMain?: (eventName: string, payload?: any) => Promise<any>
|
|
123
|
+
regChannel?: (eventName: string, handler: (data: any) => any) => () => void
|
|
124
|
+
onMain?: (eventName: string, handler: (event: any) => any) => () => void
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function unwrapPayload<T>(raw: unknown): T {
|
|
128
|
+
if (raw && typeof raw === 'object') {
|
|
129
|
+
const record = raw as Record<string, unknown>
|
|
130
|
+
if ('data' in record && 'header' in record) {
|
|
131
|
+
return (record as any).data as T
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return raw as T
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class TuffPluginTransport implements ITuffTransport {
|
|
138
|
+
private cache = new Map<string, CacheEntry>()
|
|
139
|
+
private handlers = new Map<string, Set<(payload: any) => any>>()
|
|
140
|
+
private portCache = new Map<string, TransportPortHandle>()
|
|
141
|
+
private portHandlesById = new Map<string, TransportPortHandle>()
|
|
142
|
+
private pendingPortConfirms = new Map<
|
|
143
|
+
string,
|
|
144
|
+
{ resolve: (record: PortConfirmRecord) => void, timeout?: NodeJS.Timeout }
|
|
145
|
+
>()
|
|
146
|
+
|
|
147
|
+
private queuedPortConfirms = new Map<string, PortConfirmRecord>()
|
|
148
|
+
private abandonedPorts = new Set<string>()
|
|
149
|
+
private portListenerCleanup: (() => void) | null = null
|
|
150
|
+
private portEventSubscriptions = new Map<string, PortEventSubscription>()
|
|
151
|
+
|
|
152
|
+
constructor(private readonly channel: PluginChannelLike) {}
|
|
153
|
+
|
|
154
|
+
async send<TReq, TRes>(
|
|
155
|
+
event: TuffEvent<TReq, TRes> | TuffEvent<void, TRes>,
|
|
156
|
+
payload?: TReq | void,
|
|
157
|
+
options?: SendOptions,
|
|
158
|
+
): Promise<TRes> {
|
|
159
|
+
assertTuffEvent(event as any, 'TuffPluginTransport.send')
|
|
160
|
+
|
|
161
|
+
const eventName = (event as any).toEventName() as string
|
|
162
|
+
const cacheConfig = normalizeCacheOptions(options)
|
|
163
|
+
const cacheKey = cacheConfig ? buildCacheKey(eventName, payload, cacheConfig.key) : ''
|
|
164
|
+
if (cacheConfig) {
|
|
165
|
+
const entry = this.cache.get(cacheKey)
|
|
166
|
+
if (entry) {
|
|
167
|
+
if (entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
|
|
168
|
+
return entry.value as TRes
|
|
169
|
+
}
|
|
170
|
+
this.cache.delete(cacheKey)
|
|
171
|
+
}
|
|
172
|
+
if (cacheConfig.mode === 'only') {
|
|
173
|
+
throw new Error(`[TuffTransport] Cache miss for \"${eventName}\"`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sender = typeof this.channel.sendToMain === 'function'
|
|
178
|
+
? this.channel.sendToMain.bind(this.channel)
|
|
179
|
+
: this.channel.send
|
|
180
|
+
|
|
181
|
+
if (!sender) {
|
|
182
|
+
throw new Error('[TuffPluginTransport] Channel send function not available')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const shouldPassPayload = payload !== undefined
|
|
186
|
+
const result = await sender(eventName, shouldPassPayload ? payload : undefined)
|
|
187
|
+
if (cacheConfig) {
|
|
188
|
+
const expiresAt = typeof cacheConfig.ttlMs === 'number' && Number.isFinite(cacheConfig.ttlMs)
|
|
189
|
+
? Date.now() + Math.max(0, cacheConfig.ttlMs)
|
|
190
|
+
: undefined
|
|
191
|
+
this.cache.set(cacheKey, { value: result, expiresAt })
|
|
192
|
+
}
|
|
193
|
+
return result as TRes
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private formatPortErrorMessage(error?: unknown): string | null {
|
|
197
|
+
if (!error)
|
|
198
|
+
return null
|
|
199
|
+
const raw = error instanceof Error ? error.message : String(error)
|
|
200
|
+
if (!raw)
|
|
201
|
+
return null
|
|
202
|
+
const normalized = raw.replace(/\s+/g, ' ').trim()
|
|
203
|
+
if (!normalized)
|
|
204
|
+
return null
|
|
205
|
+
return normalized.length > 200 ? `${normalized.slice(0, 200)}...` : normalized
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private logPortIssue(channel: string, reason: string, error?: unknown): void {
|
|
209
|
+
const channelName = channel.trim() || 'unknown'
|
|
210
|
+
const detail = this.formatPortErrorMessage(error)
|
|
211
|
+
const suffix = detail ? `: ${detail}` : ''
|
|
212
|
+
console.warn(`[TuffTransport] Port issue for \"${channelName}\": ${reason}${suffix}`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private ensurePortListener(): boolean {
|
|
216
|
+
if (this.portListenerCleanup) {
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const ipcRenderer = resolveIpcRenderer()
|
|
221
|
+
if (!ipcRenderer?.on || !ipcRenderer.removeListener) {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const eventName = TransportEvents.port.confirm.toEventName()
|
|
226
|
+
const handler = (event: any, payload: TransportPortConfirmPayload) => {
|
|
227
|
+
this.handlePortConfirm(event, payload)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
ipcRenderer.on(eventName, handler)
|
|
231
|
+
this.portListenerCleanup = () => {
|
|
232
|
+
ipcRenderer.removeListener?.(eventName, handler)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private handlePortConfirm(event: any, payload: TransportPortConfirmPayload): void {
|
|
239
|
+
const portId = payload?.portId
|
|
240
|
+
const channel = payload?.channel
|
|
241
|
+
const port = event?.ports?.[0] as MessagePort | undefined
|
|
242
|
+
if (!portId || !channel || !port) {
|
|
243
|
+
this.logPortIssue(channel ?? 'unknown', 'confirm_payload_invalid')
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.abandonedPorts.has(portId)) {
|
|
248
|
+
this.abandonedPorts.delete(portId)
|
|
249
|
+
try {
|
|
250
|
+
port.close()
|
|
251
|
+
}
|
|
252
|
+
catch {}
|
|
253
|
+
void this.send(TransportEvents.port.close, { channel, portId, reason: 'confirm_timeout' }).catch(() => {})
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const record: PortConfirmRecord = { port, payload }
|
|
258
|
+
const pending = this.pendingPortConfirms.get(portId)
|
|
259
|
+
if (pending) {
|
|
260
|
+
this.pendingPortConfirms.delete(portId)
|
|
261
|
+
if (pending.timeout) {
|
|
262
|
+
clearTimeout(pending.timeout)
|
|
263
|
+
}
|
|
264
|
+
pending.resolve(record)
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.queuedPortConfirms.set(portId, record)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void this.send(TransportEvents.port.confirm, payload).catch(() => {})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private waitForPortConfirm(
|
|
274
|
+
portId: string,
|
|
275
|
+
channel: string,
|
|
276
|
+
timeoutMs: number,
|
|
277
|
+
): Promise<PortConfirmRecord | null> {
|
|
278
|
+
const queued = this.queuedPortConfirms.get(portId)
|
|
279
|
+
if (queued) {
|
|
280
|
+
this.queuedPortConfirms.delete(portId)
|
|
281
|
+
return Promise.resolve(queued)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
const timeout = setTimeout(() => {
|
|
286
|
+
this.pendingPortConfirms.delete(portId)
|
|
287
|
+
this.abandonedPorts.add(portId)
|
|
288
|
+
this.logPortIssue(channel, 'confirm_timeout')
|
|
289
|
+
resolve(null)
|
|
290
|
+
}, timeoutMs)
|
|
291
|
+
|
|
292
|
+
this.pendingPortConfirms.set(portId, { resolve, timeout })
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private evictPortHandle(portId: string, channel?: string): void {
|
|
297
|
+
this.portHandlesById.delete(portId)
|
|
298
|
+
if (channel) {
|
|
299
|
+
const cached = this.portCache.get(channel)
|
|
300
|
+
if (cached?.portId === portId) {
|
|
301
|
+
this.portCache.delete(channel)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private buildPortHandle(record: PortConfirmRecord, cache: boolean): TransportPortHandle {
|
|
307
|
+
const { port, payload } = record
|
|
308
|
+
const portId = payload.portId
|
|
309
|
+
const channel = payload.channel
|
|
310
|
+
|
|
311
|
+
let closing = false
|
|
312
|
+
|
|
313
|
+
const handle: TransportPortHandle = {
|
|
314
|
+
portId,
|
|
315
|
+
channel,
|
|
316
|
+
port,
|
|
317
|
+
close: async (reason?: string) => {
|
|
318
|
+
closing = true
|
|
319
|
+
this.evictPortHandle(portId, channel)
|
|
320
|
+
try {
|
|
321
|
+
port.close()
|
|
322
|
+
}
|
|
323
|
+
catch {}
|
|
324
|
+
await this.send(TransportEvents.port.close, {
|
|
325
|
+
channel,
|
|
326
|
+
portId,
|
|
327
|
+
reason,
|
|
328
|
+
}).catch(() => {})
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.portHandlesById.set(portId, handle)
|
|
333
|
+
if (cache) {
|
|
334
|
+
this.portCache.set(channel, handle)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const onClose = () => {
|
|
338
|
+
this.evictPortHandle(portId, channel)
|
|
339
|
+
if (!closing) {
|
|
340
|
+
this.logPortIssue(channel, 'port_closed')
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const onMessageError = () => {
|
|
345
|
+
this.logPortIssue(channel, 'message_error')
|
|
346
|
+
this.evictPortHandle(portId, channel)
|
|
347
|
+
void this.send(TransportEvents.port.error, {
|
|
348
|
+
channel,
|
|
349
|
+
portId,
|
|
350
|
+
error: { code: 'message_error', message: 'MessagePort messageerror' },
|
|
351
|
+
}).catch(() => {})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (typeof port.addEventListener === 'function') {
|
|
355
|
+
port.addEventListener('close', onClose)
|
|
356
|
+
port.addEventListener('messageerror', onMessageError)
|
|
357
|
+
}
|
|
358
|
+
if (typeof port.start === 'function') {
|
|
359
|
+
port.start()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return handle
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private normalizePortEventMessage<TReq>(raw: unknown, channel: string): TReq | null {
|
|
366
|
+
if (!raw || typeof raw !== 'object') {
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const record = raw as TransportPortEnvelope<TReq> & { payload?: TReq }
|
|
371
|
+
if (record.channel && record.channel !== channel) {
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (record.type && record.type !== 'data') {
|
|
376
|
+
if (record.type === 'error' && record.error?.message) {
|
|
377
|
+
this.logPortIssue(channel, 'message_error', record.error.message)
|
|
378
|
+
}
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (record.payload !== undefined) {
|
|
383
|
+
return record.payload as TReq
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private dropPortEventSubscription(channel: string): void {
|
|
390
|
+
const subscription = this.portEventSubscriptions.get(channel)
|
|
391
|
+
if (!subscription)
|
|
392
|
+
return
|
|
393
|
+
subscription.cleanup?.()
|
|
394
|
+
subscription.cleanup = null
|
|
395
|
+
subscription.handle = null
|
|
396
|
+
subscription.opening = null
|
|
397
|
+
subscription.closing = true
|
|
398
|
+
this.portEventSubscriptions.delete(channel)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private ensurePortEventSubscription(channel: string): void {
|
|
402
|
+
if (!isPortChannelEnabled(channel)) {
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
const existing = this.portEventSubscriptions.get(channel)
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.refCount += 1
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const subscription: PortEventSubscription = {
|
|
412
|
+
refCount: 1,
|
|
413
|
+
handle: null,
|
|
414
|
+
cleanup: null,
|
|
415
|
+
opening: null,
|
|
416
|
+
closing: false,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.portEventSubscriptions.set(channel, subscription)
|
|
420
|
+
|
|
421
|
+
subscription.opening = this.openPort({ channel })
|
|
422
|
+
.then((handle) => {
|
|
423
|
+
if (!handle) {
|
|
424
|
+
this.logPortIssue(channel, 'port_unavailable')
|
|
425
|
+
this.dropPortEventSubscription(channel)
|
|
426
|
+
return null
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (subscription.closing) {
|
|
430
|
+
void handle.close('subscription_closed')
|
|
431
|
+
this.dropPortEventSubscription(channel)
|
|
432
|
+
return null
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
subscription.handle = handle
|
|
436
|
+
const port = handle.port
|
|
437
|
+
|
|
438
|
+
const messageHandler = (event: MessageEvent) => {
|
|
439
|
+
const payload = this.normalizePortEventMessage<any>(event?.data, channel)
|
|
440
|
+
if (payload === null)
|
|
441
|
+
return
|
|
442
|
+
const handlers = this.handlers.get(channel)
|
|
443
|
+
if (!handlers || handlers.size === 0)
|
|
444
|
+
return
|
|
445
|
+
handlers.forEach((handler) => {
|
|
446
|
+
Promise.resolve(handler(payload)).catch((error) => {
|
|
447
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
448
|
+
console.error(`[TuffTransport] Handler error for \"${channel}\":`, errorMessage)
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const closeHandler = () => {
|
|
454
|
+
this.logPortIssue(channel, 'port_closed')
|
|
455
|
+
this.dropPortEventSubscription(channel)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const errorHandler = () => {
|
|
459
|
+
this.logPortIssue(channel, 'message_error')
|
|
460
|
+
this.dropPortEventSubscription(channel)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (typeof port.addEventListener === 'function') {
|
|
464
|
+
port.addEventListener('message', messageHandler)
|
|
465
|
+
port.addEventListener('messageerror', errorHandler)
|
|
466
|
+
port.addEventListener('close', closeHandler)
|
|
467
|
+
port.start?.()
|
|
468
|
+
subscription.cleanup = () => {
|
|
469
|
+
port.removeEventListener('message', messageHandler)
|
|
470
|
+
port.removeEventListener('messageerror', errorHandler)
|
|
471
|
+
port.removeEventListener('close', closeHandler)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
port.onmessage = messageHandler as any
|
|
476
|
+
subscription.cleanup = () => {
|
|
477
|
+
port.onmessage = null
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return handle
|
|
482
|
+
})
|
|
483
|
+
.catch((error) => {
|
|
484
|
+
this.logPortIssue(channel, 'open_failed', error)
|
|
485
|
+
this.dropPortEventSubscription(channel)
|
|
486
|
+
return null
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private releasePortEventSubscription(channel: string): void {
|
|
491
|
+
const subscription = this.portEventSubscriptions.get(channel)
|
|
492
|
+
if (!subscription)
|
|
493
|
+
return
|
|
494
|
+
subscription.refCount -= 1
|
|
495
|
+
if (subscription.refCount > 0)
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
subscription.closing = true
|
|
499
|
+
subscription.cleanup?.()
|
|
500
|
+
subscription.cleanup = null
|
|
501
|
+
if (subscription.handle) {
|
|
502
|
+
void subscription.handle.close('no_handlers')
|
|
503
|
+
}
|
|
504
|
+
this.portEventSubscriptions.delete(channel)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async upgrade(options: TransportPortUpgradeRequest): Promise<TransportPortUpgradeResponse> {
|
|
508
|
+
return await this.send(TransportEvents.port.upgrade, options)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async openPort(options: TransportPortOpenOptions): Promise<TransportPortHandle | null> {
|
|
512
|
+
const channel = options?.channel?.trim()
|
|
513
|
+
if (!channel) {
|
|
514
|
+
this.logPortIssue('unknown', 'missing_channel')
|
|
515
|
+
return null
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const useCache = options.force !== true
|
|
520
|
+
if (useCache) {
|
|
521
|
+
const cached = this.portCache.get(channel)
|
|
522
|
+
if (cached) {
|
|
523
|
+
return cached
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!this.ensurePortListener()) {
|
|
528
|
+
this.logPortIssue(channel, 'ipc_unavailable')
|
|
529
|
+
return null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const pluginName = options.plugin ?? resolvePluginName()
|
|
533
|
+
const scope = options.scope ?? (pluginName ? 'plugin' : 'window')
|
|
534
|
+
if (scope === 'plugin' && !pluginName) {
|
|
535
|
+
this.logPortIssue(channel, 'plugin_required')
|
|
536
|
+
return null
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const response = await this.upgrade({
|
|
540
|
+
channel,
|
|
541
|
+
scope,
|
|
542
|
+
windowId: options.windowId,
|
|
543
|
+
plugin: scope === 'plugin' ? pluginName : options.plugin,
|
|
544
|
+
permissions: options.permissions,
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
if (!response.accepted || !response.portId) {
|
|
548
|
+
if (response.error) {
|
|
549
|
+
const reason = response.error.code ? `upgrade_rejected:${response.error.code}` : 'upgrade_rejected'
|
|
550
|
+
this.logPortIssue(channel, reason, response.error.message)
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
this.logPortIssue(channel, 'upgrade_rejected')
|
|
554
|
+
}
|
|
555
|
+
return null
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const timeoutMs = typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs)
|
|
559
|
+
? Math.max(0, options.timeoutMs)
|
|
560
|
+
: PORT_CONFIRM_TIMEOUT_MS
|
|
561
|
+
const record = await this.waitForPortConfirm(response.portId, channel, timeoutMs)
|
|
562
|
+
if (!record) {
|
|
563
|
+
this.logPortIssue(channel, 'confirm_timeout')
|
|
564
|
+
return null
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return this.buildPortHandle(record, useCache)
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
this.logPortIssue(channel, 'open_failed', error)
|
|
571
|
+
return null
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
stream<TReq, TChunk>(
|
|
576
|
+
_event: TuffEvent<TReq, AsyncIterable<TChunk>>,
|
|
577
|
+
_payload: TReq,
|
|
578
|
+
_options: StreamOptions<TChunk>,
|
|
579
|
+
): Promise<StreamController> {
|
|
580
|
+
throw new Error('[TuffPluginTransport] Stream is not supported in plugin transport')
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
on<TReq, TRes>(
|
|
584
|
+
event: TuffEvent<TReq, TRes>,
|
|
585
|
+
handler: (payload: TReq) => TRes | Promise<TRes>,
|
|
586
|
+
): () => void {
|
|
587
|
+
assertTuffEvent(event, 'TuffPluginTransport.on')
|
|
588
|
+
|
|
589
|
+
const eventName = event.toEventName()
|
|
590
|
+
const handlerSet = this.handlers.get(eventName) || new Set()
|
|
591
|
+
const isFirstHandler = handlerSet.size === 0
|
|
592
|
+
handlerSet.add(handler)
|
|
593
|
+
this.handlers.set(eventName, handlerSet)
|
|
594
|
+
if (isFirstHandler) {
|
|
595
|
+
this.ensurePortEventSubscription(eventName)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let cleanupChannel: (() => void) | null = null
|
|
599
|
+
if (typeof this.channel.onMain === 'function') {
|
|
600
|
+
cleanupChannel = this.channel.onMain(eventName, raw => handler(unwrapPayload<TReq>(raw)))
|
|
601
|
+
}
|
|
602
|
+
else if (typeof this.channel.regChannel === 'function') {
|
|
603
|
+
cleanupChannel = this.channel.regChannel(eventName, raw => handler(unwrapPayload<TReq>(raw)))
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
throw new TypeError('[TuffPluginTransport] Channel on function not available')
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return () => {
|
|
610
|
+
handlerSet.delete(handler)
|
|
611
|
+
if (handlerSet.size === 0) {
|
|
612
|
+
this.handlers.delete(eventName)
|
|
613
|
+
this.releasePortEventSubscription(eventName)
|
|
614
|
+
}
|
|
615
|
+
cleanupChannel?.()
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async flush(): Promise<void> {
|
|
620
|
+
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
destroy(): void {
|
|
624
|
+
this.handlers.clear()
|
|
625
|
+
|
|
626
|
+
if (this.portListenerCleanup) {
|
|
627
|
+
this.portListenerCleanup()
|
|
628
|
+
this.portListenerCleanup = null
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
for (const subscription of this.portEventSubscriptions.values()) {
|
|
632
|
+
subscription.cleanup?.()
|
|
633
|
+
subscription.cleanup = null
|
|
634
|
+
if (subscription.handle) {
|
|
635
|
+
void subscription.handle.close('transport_destroy').catch(() => {})
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
this.portEventSubscriptions.clear()
|
|
639
|
+
|
|
640
|
+
for (const handle of this.portHandlesById.values()) {
|
|
641
|
+
void handle.close('transport_destroy').catch(() => {})
|
|
642
|
+
}
|
|
643
|
+
this.portHandlesById.clear()
|
|
644
|
+
this.portCache.clear()
|
|
645
|
+
this.pendingPortConfirms.clear()
|
|
646
|
+
this.queuedPortConfirms.clear()
|
|
647
|
+
this.abandonedPorts.clear()
|
|
648
|
+
this.cache.clear()
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function createPluginTuffTransport(channel: PluginChannelLike): ITuffTransport {
|
|
653
|
+
return new TuffPluginTransport(channel)
|
|
654
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getEnv } from '../../env'
|
|
2
|
+
import { AppEvents, ClipboardEvents, CoreBoxEvents } from '../events'
|
|
3
|
+
|
|
4
|
+
const PORT_CHANNELS_ENV = 'TALEX_TRANSPORT_PORT_CHANNELS'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PORT_CHANNELS = new Set<string>([
|
|
7
|
+
ClipboardEvents.change.toEventName(),
|
|
8
|
+
AppEvents.fileIndex.progress.toEventName(),
|
|
9
|
+
CoreBoxEvents.search.update.toEventName(),
|
|
10
|
+
CoreBoxEvents.search.end.toEventName(),
|
|
11
|
+
CoreBoxEvents.search.noResults.toEventName(),
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
let cachedRaw: string | undefined
|
|
15
|
+
let cachedAllowlist: ReadonlySet<string> | null = null
|
|
16
|
+
|
|
17
|
+
function parsePortChannels(raw: string): ReadonlySet<string> {
|
|
18
|
+
const trimmed = raw.trim()
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
return new Set()
|
|
21
|
+
}
|
|
22
|
+
const entries = trimmed.split(/[,;\s]+/).map(item => item.trim()).filter(Boolean)
|
|
23
|
+
return new Set(entries)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolvePortChannelAllowlist(): ReadonlySet<string> {
|
|
27
|
+
const raw = getEnv(PORT_CHANNELS_ENV)
|
|
28
|
+
if (cachedAllowlist && raw === cachedRaw) {
|
|
29
|
+
return cachedAllowlist
|
|
30
|
+
}
|
|
31
|
+
cachedRaw = raw
|
|
32
|
+
cachedAllowlist = raw === undefined ? DEFAULT_PORT_CHANNELS : parsePortChannels(raw)
|
|
33
|
+
return cachedAllowlist
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isPortChannelEnabled(channel: string): boolean {
|
|
37
|
+
return resolvePortChannelAllowlist().has(channel)
|
|
38
|
+
}
|