@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.
Files changed (234) 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 +32 -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 +9 -5
  72. package/market/index.ts +1 -1
  73. package/market/types.ts +19 -4
  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 +80 -7
  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 -4
  87. package/plugin/providers/market-client.ts +6 -3
  88. package/plugin/providers/npm-provider.ts +22 -7
  89. package/plugin/providers/tpex-provider.ts +22 -8
  90. package/plugin/sdk/box-items.ts +14 -0
  91. package/plugin/sdk/box-sdk.ts +64 -0
  92. package/plugin/sdk/channel.ts +119 -4
  93. package/plugin/sdk/clipboard.ts +26 -12
  94. package/plugin/sdk/cloud-sync.ts +113 -0
  95. package/plugin/sdk/common.ts +19 -11
  96. package/plugin/sdk/core-box.ts +6 -15
  97. package/plugin/sdk/division-box.ts +160 -65
  98. package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
  99. package/plugin/sdk/feature-sdk.ts +111 -76
  100. package/plugin/sdk/flow.ts +146 -45
  101. package/plugin/sdk/hooks/bridge.ts +13 -6
  102. package/plugin/sdk/hooks/life-cycle.ts +35 -16
  103. package/plugin/sdk/index.ts +14 -3
  104. package/plugin/sdk/intelligence.ts +87 -0
  105. package/plugin/sdk/meta/README.md +179 -0
  106. package/plugin/sdk/meta-sdk.ts +244 -0
  107. package/plugin/sdk/notification.ts +9 -0
  108. package/plugin/sdk/plugin-info.ts +64 -0
  109. package/plugin/sdk/power.ts +155 -0
  110. package/plugin/sdk/recommend.ts +21 -0
  111. package/plugin/sdk/service/index.ts +12 -8
  112. package/plugin/sdk/sqlite.ts +141 -0
  113. package/plugin/sdk/storage.ts +2 -6
  114. package/plugin/sdk/system.ts +2 -9
  115. package/plugin/sdk/temp-files.ts +41 -0
  116. package/plugin/sdk/touch-sdk.ts +18 -0
  117. package/plugin/sdk/types.ts +44 -4
  118. package/plugin/sdk/window/index.ts +12 -9
  119. package/plugin/sdk-version.ts +231 -0
  120. package/preload/renderer.ts +3 -2
  121. package/renderer/hooks/arg-mapper.ts +16 -2
  122. package/renderer/hooks/index.ts +13 -0
  123. package/renderer/hooks/initialize.ts +2 -1
  124. package/renderer/hooks/use-agent-market-sdk.ts +7 -0
  125. package/renderer/hooks/use-agent-market.ts +106 -0
  126. package/renderer/hooks/use-agents-sdk.ts +7 -0
  127. package/renderer/hooks/use-app-sdk.ts +7 -0
  128. package/renderer/hooks/use-channel.ts +33 -4
  129. package/renderer/hooks/use-download-sdk.ts +21 -0
  130. package/renderer/hooks/use-intelligence-sdk.ts +7 -0
  131. package/renderer/hooks/use-intelligence-stats.ts +290 -0
  132. package/renderer/hooks/use-intelligence.ts +55 -214
  133. package/renderer/hooks/use-market-sdk.ts +16 -0
  134. package/renderer/hooks/use-notification-sdk.ts +7 -0
  135. package/renderer/hooks/use-permission-sdk.ts +7 -0
  136. package/renderer/hooks/use-permission.ts +325 -0
  137. package/renderer/hooks/use-platform-sdk.ts +7 -0
  138. package/renderer/hooks/use-plugin-sdk.ts +16 -0
  139. package/renderer/hooks/use-settings-sdk.ts +7 -0
  140. package/renderer/hooks/use-update-sdk.ts +21 -0
  141. package/renderer/index.ts +1 -0
  142. package/renderer/ref.ts +19 -10
  143. package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
  144. package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
  145. package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
  146. package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
  147. package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
  148. package/renderer/shared/components/index.ts +5 -0
  149. package/renderer/shared/components/shims-vue.d.ts +5 -0
  150. package/renderer/shared/index.ts +2 -0
  151. package/renderer/shared/plugin-detail.ts +62 -0
  152. package/renderer/storage/app-settings.ts +3 -1
  153. package/renderer/storage/base-storage.ts +508 -82
  154. package/renderer/storage/intelligence-storage.ts +31 -40
  155. package/renderer/storage/openers.ts +3 -1
  156. package/renderer/storage/storage-subscription.ts +126 -42
  157. package/renderer/touch-sdk/env.ts +10 -10
  158. package/renderer/touch-sdk/index.ts +114 -18
  159. package/renderer/touch-sdk/terminal.ts +24 -13
  160. package/search/feature-matcher.ts +279 -0
  161. package/search/fuzzy-match.ts +64 -34
  162. package/search/index.ts +10 -0
  163. package/search/levenshtein-utils.ts +17 -11
  164. package/transport/errors.ts +310 -0
  165. package/transport/event/builder.ts +378 -0
  166. package/transport/event/index.ts +7 -0
  167. package/transport/event/types.ts +292 -0
  168. package/transport/events/index.ts +2690 -0
  169. package/transport/events/meta-overlay.ts +79 -0
  170. package/transport/events/types/agents.ts +177 -0
  171. package/transport/events/types/app-index.ts +20 -0
  172. package/transport/events/types/app.ts +475 -0
  173. package/transport/events/types/box-item.ts +222 -0
  174. package/transport/events/types/clipboard.ts +80 -0
  175. package/transport/events/types/core-box.ts +534 -0
  176. package/transport/events/types/device-idle.ts +7 -0
  177. package/transport/events/types/division-box.ts +99 -0
  178. package/transport/events/types/download.ts +115 -0
  179. package/transport/events/types/file-index.ts +84 -0
  180. package/transport/events/types/flow.ts +149 -0
  181. package/transport/events/types/index.ts +70 -0
  182. package/transport/events/types/market.ts +39 -0
  183. package/transport/events/types/meta-overlay.ts +184 -0
  184. package/transport/events/types/notification.ts +140 -0
  185. package/transport/events/types/permission.ts +90 -0
  186. package/transport/events/types/platform.ts +8 -0
  187. package/transport/events/types/plugin.ts +631 -0
  188. package/transport/events/types/sentry.ts +20 -0
  189. package/transport/events/types/storage.ts +208 -0
  190. package/transport/events/types/transport.ts +60 -0
  191. package/transport/events/types/tray.ts +16 -0
  192. package/transport/events/types/update.ts +78 -0
  193. package/transport/index.ts +141 -0
  194. package/transport/main.ts +2 -0
  195. package/transport/prelude.ts +208 -0
  196. package/transport/sdk/constants.ts +29 -0
  197. package/transport/sdk/domains/agents-market.ts +47 -0
  198. package/transport/sdk/domains/agents.ts +62 -0
  199. package/transport/sdk/domains/app.ts +48 -0
  200. package/transport/sdk/domains/disposable.ts +35 -0
  201. package/transport/sdk/domains/download.ts +139 -0
  202. package/transport/sdk/domains/index.ts +13 -0
  203. package/transport/sdk/domains/intelligence.ts +616 -0
  204. package/transport/sdk/domains/market.ts +35 -0
  205. package/transport/sdk/domains/notification.ts +62 -0
  206. package/transport/sdk/domains/permission.ts +85 -0
  207. package/transport/sdk/domains/platform.ts +19 -0
  208. package/transport/sdk/domains/plugin.ts +144 -0
  209. package/transport/sdk/domains/settings.ts +102 -0
  210. package/transport/sdk/domains/update.ts +64 -0
  211. package/transport/sdk/index.ts +60 -0
  212. package/transport/sdk/main-transport.ts +710 -0
  213. package/transport/sdk/main.ts +9 -0
  214. package/transport/sdk/plugin-transport.ts +654 -0
  215. package/transport/sdk/port-policy.ts +38 -0
  216. package/transport/sdk/renderer-transport.ts +1165 -0
  217. package/transport/types.ts +605 -0
  218. package/types/agent.ts +399 -0
  219. package/types/cloud-sync.ts +157 -0
  220. package/types/division-box.ts +31 -31
  221. package/types/download.ts +1 -0
  222. package/types/flow.ts +63 -12
  223. package/types/icon.ts +2 -1
  224. package/types/index.ts +5 -0
  225. package/types/intelligence.ts +166 -173
  226. package/types/modules/base.ts +2 -0
  227. package/types/path-browserify.d.ts +5 -0
  228. package/types/platform.ts +12 -0
  229. package/types/startup-info.ts +32 -0
  230. package/types/touch-app-core.ts +8 -8
  231. package/types/update.ts +94 -1
  232. package/vitest.config.ts +25 -0
  233. package/auth/useClerkConfig.ts +0 -40
  234. 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
+ }