@talex-touch/utils 1.0.22 → 1.0.23

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.
@@ -166,6 +166,8 @@ export function structuredStrictStringify(value: unknown): string {
166
166
  return JSON.stringify(serialize(value, 'root'));
167
167
  }
168
168
 
169
+ export * from './timing'
170
+
169
171
  export { runAdaptiveTaskQueue, type AdaptiveTaskQueueOptions } from './task-queue'
170
172
 
171
173
  export * from './time'
@@ -0,0 +1,250 @@
1
+ export type TimingMeta = Record<string, unknown>
2
+
3
+ export interface TimingRecord {
4
+ label: string
5
+ durationMs: number
6
+ startedAt: number
7
+ endedAt: number
8
+ iteration?: number
9
+ meta?: TimingMeta
10
+ error?: unknown
11
+ }
12
+
13
+ export interface TimingStats {
14
+ label: string
15
+ count: number
16
+ totalMs: number
17
+ avgMs: number
18
+ maxMs: number
19
+ minMs: number
20
+ lastMs: number
21
+ lastStartedAt?: number
22
+ lastEndedAt?: number
23
+ errorCount: number
24
+ lastError?: unknown
25
+ }
26
+
27
+ export interface TimingSummary extends TimingStats {
28
+ history: TimingRecord[]
29
+ }
30
+
31
+ export interface TimingManagerConfig {
32
+ autoLog?: boolean
33
+ historyLimit?: number
34
+ logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
35
+ formatter?: (entry: TimingRecord, stats: TimingStats) => string
36
+ }
37
+
38
+ export interface TimingOptions {
39
+ autoLog?: boolean
40
+ storeHistory?: boolean
41
+ logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
42
+ formatter?: (entry: TimingRecord, stats: TimingStats) => string
43
+ historyLimit?: number
44
+ }
45
+
46
+ export class TimingManager {
47
+ private readonly stats = new Map<string, TimingStats>()
48
+ private readonly history = new Map<string, TimingRecord[]>()
49
+ private readonly moduleStats = new Map<string, TimingStats>()
50
+
51
+ constructor(private readonly config: TimingManagerConfig = {}) {}
52
+
53
+ createTiming(label: string, options: TimingOptions = {}): TimingScope {
54
+ return new TimingScope(this, label, options)
55
+ }
56
+
57
+ record(label: string, record: TimingRecord, options: TimingOptions = {}): void {
58
+ const stats = this.updateStats(this.stats, label, record)
59
+ const moduleKey = this.extractModuleKey(label)
60
+ this.updateStats(this.moduleStats, moduleKey, { ...record, label: moduleKey })
61
+
62
+ if (options.storeHistory ?? true) {
63
+ const limit = options.historyLimit ?? this.config.historyLimit ?? 50
64
+ const list = this.history.get(label) ?? []
65
+ list.push(record)
66
+ if (list.length > limit) {
67
+ list.splice(0, list.length - limit)
68
+ }
69
+ this.history.set(label, list)
70
+ }
71
+
72
+ const shouldLog = options.autoLog ?? this.config.autoLog ?? true
73
+ if (shouldLog) {
74
+ const formatter = options.formatter ?? this.config.formatter ?? defaultFormatter
75
+ const logger = options.logger ?? this.config.logger ?? defaultLogger
76
+ logger(formatter(record, stats), record, stats)
77
+ }
78
+ }
79
+
80
+ getStats(label: string): TimingStats | undefined {
81
+ const stats = this.stats.get(label)
82
+ if (!stats) return undefined
83
+ return { ...stats }
84
+ }
85
+
86
+ getAllStats(): TimingStats[] {
87
+ return Array.from(this.stats.values()).map((s) => ({ ...s }))
88
+ }
89
+
90
+ getHistory(label: string): TimingRecord[] {
91
+ return [...(this.history.get(label) ?? [])]
92
+ }
93
+
94
+ getModuleStats(moduleKey?: string): TimingStats[] | TimingStats | undefined {
95
+ if (moduleKey) {
96
+ const stats = this.moduleStats.get(moduleKey)
97
+ return stats ? { ...stats } : undefined
98
+ }
99
+ return Array.from(this.moduleStats.values()).map((s) => ({ ...s }))
100
+ }
101
+
102
+ reset(label?: string): void {
103
+ if (!label) {
104
+ this.stats.clear()
105
+ this.history.clear()
106
+ this.moduleStats.clear()
107
+ return
108
+ }
109
+ this.stats.delete(label)
110
+ this.history.delete(label)
111
+ }
112
+
113
+ private updateStats(target: Map<string, TimingStats>, label: string, record: TimingRecord): TimingStats {
114
+ const { durationMs, error } = record
115
+ const next = target.get(label) ?? {
116
+ label,
117
+ count: 0,
118
+ totalMs: 0,
119
+ avgMs: 0,
120
+ maxMs: Number.NEGATIVE_INFINITY,
121
+ minMs: Number.POSITIVE_INFINITY,
122
+ lastMs: 0,
123
+ errorCount: 0
124
+ }
125
+
126
+ next.count += 1
127
+ next.totalMs += durationMs
128
+ next.avgMs = next.totalMs / next.count
129
+ next.maxMs = Math.max(next.maxMs, durationMs)
130
+ next.minMs = Math.min(next.minMs, durationMs)
131
+ next.lastMs = durationMs
132
+ next.lastStartedAt = record.startedAt
133
+ next.lastEndedAt = record.endedAt
134
+
135
+ if (error) {
136
+ next.errorCount += 1
137
+ next.lastError = error
138
+ }
139
+
140
+ target.set(label, next)
141
+ return next
142
+ }
143
+
144
+ private extractModuleKey(label: string): string {
145
+ const [moduleKey] = label.split(':')
146
+ return moduleKey || label
147
+ }
148
+ }
149
+
150
+ export class TimingScope {
151
+ constructor(
152
+ private readonly manager: TimingManager,
153
+ private readonly label: string,
154
+ private readonly options: TimingOptions
155
+ ) {}
156
+
157
+ async cost<T>(fn: () => Promise<T> | T, meta: TimingMeta = {}, overrides: TimingOptions = {}): Promise<T> {
158
+ const startedAt = now()
159
+ try {
160
+ const result = await fn()
161
+ this.finish(startedAt, meta, undefined, overrides)
162
+ return result
163
+ } catch (error) {
164
+ this.finish(startedAt, meta, error, overrides)
165
+ throw error
166
+ }
167
+ }
168
+
169
+ async count<T>(iterations: number, fn: (iteration: number) => Promise<T> | T, meta: TimingMeta | ((iteration: number) => TimingMeta) = {}, overrides: TimingOptions = {}): Promise<T[]> {
170
+ const results: T[] = []
171
+ for (let index = 0; index < iterations; index++) {
172
+ const iterationMeta = typeof meta === 'function' ? meta(index) : meta
173
+ const startedAt = now()
174
+ try {
175
+ const value = await fn(index)
176
+ this.finish(startedAt, { ...iterationMeta, iteration: index }, undefined, overrides)
177
+ results.push(value)
178
+ } catch (error) {
179
+ this.finish(startedAt, { ...iterationMeta, iteration: index }, error, overrides)
180
+ throw error
181
+ }
182
+ }
183
+ return results
184
+ }
185
+
186
+ mark(durationMs: number, meta: TimingMeta = {}, overrides: TimingOptions = {}): void {
187
+ const endedAt = now()
188
+ const startedAt = endedAt - durationMs
189
+ this.manager.record(
190
+ this.label,
191
+ {
192
+ label: this.label,
193
+ durationMs,
194
+ startedAt,
195
+ endedAt,
196
+ meta
197
+ },
198
+ { ...this.options, ...overrides }
199
+ )
200
+ }
201
+
202
+ getStats(): TimingStats | undefined {
203
+ return this.manager.getStats(this.label)
204
+ }
205
+
206
+ getHistory(): TimingRecord[] {
207
+ return this.manager.getHistory(this.label)
208
+ }
209
+
210
+ private finish(startedAt: number, meta: TimingMeta, error: unknown, overrides: TimingOptions): void {
211
+ const endedAt = now()
212
+ const record: TimingRecord = {
213
+ label: this.label,
214
+ durationMs: endedAt - startedAt,
215
+ startedAt,
216
+ endedAt,
217
+ meta,
218
+ error
219
+ }
220
+
221
+ this.manager.record(this.label, record, { ...this.options, ...overrides })
222
+ }
223
+ }
224
+
225
+ function defaultFormatter(record: TimingRecord, stats: TimingStats): string {
226
+ const duration = record.durationMs.toFixed(2)
227
+ return `⏱ [${record.label}] ${duration} ms (avg: ${stats.avgMs.toFixed(2)} ms, max: ${stats.maxMs.toFixed(2)} ms, count: ${stats.count})`
228
+ }
229
+
230
+ function defaultLogger(message: string): void {
231
+ console.log(message)
232
+ }
233
+
234
+ const now = (() => {
235
+ if (typeof globalThis !== 'undefined') {
236
+ const perf = (globalThis as typeof globalThis & { performance?: { now?: () => number } }).performance
237
+ if (perf?.now) {
238
+ return () => perf.now()
239
+ }
240
+ }
241
+ return () => Date.now()
242
+ })()
243
+
244
+ const timingManagerInstance = new TimingManager()
245
+
246
+ export const timingManager = timingManagerInstance
247
+
248
+ export function createTiming(label: string, options: TimingOptions = {}): TimingScope {
249
+ return timingManagerInstance.createTiming(label, options)
250
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "module": "./index.ts",
6
6
  "license": "MPL-2.0",
7
7
  "private": false,
8
- "version": "1.0.22",
8
+ "version": "1.0.23",
9
9
  "scripts": {
10
10
  "publish": "npm publish --access public"
11
11
  },
@@ -8,6 +8,19 @@ const ensurePluginChannel = () => {
8
8
  return channel
9
9
  }
10
10
 
11
+ const normalizeItem = (item: PluginClipboardItem | null): PluginClipboardItem | null => {
12
+ if (!item) return item
13
+ if (!item.meta && typeof item.metadata === 'string') {
14
+ try {
15
+ const parsed = JSON.parse(item.metadata)
16
+ return { ...item, meta: parsed }
17
+ } catch {
18
+ return { ...item, meta: null }
19
+ }
20
+ }
21
+ return item
22
+ }
23
+
11
24
  export interface ClipboardHistoryOptions {
12
25
  page?: number
13
26
  }
@@ -21,17 +34,35 @@ export interface ClipboardDeleteOptions {
21
34
  id: number
22
35
  }
23
36
 
37
+ export interface ClipboardApplyOptions {
38
+ item?: PluginClipboardItem
39
+ text?: string
40
+ html?: string | null
41
+ files?: string[]
42
+ delayMs?: number
43
+ hideCoreBox?: boolean
44
+ type?: PluginClipboardItem['type']
45
+ }
46
+
24
47
  export function useClipboardHistory() {
25
48
  const channel = ensurePluginChannel()
26
49
 
27
50
  return {
28
51
  async getLatest(): Promise<PluginClipboardItem | null> {
29
- return channel.send('clipboard:get-latest')
52
+ const result = await channel.send('clipboard:get-latest')
53
+ return normalizeItem(result)
30
54
  },
31
55
 
32
56
  async getHistory(options: ClipboardHistoryOptions = {}): Promise<PluginClipboardHistoryResponse> {
33
57
  const { page = 1 } = options
34
- return channel.send('clipboard:get-history', { page })
58
+ const response = await channel.send('clipboard:get-history', { page })
59
+ const history = Array.isArray(response?.history)
60
+ ? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
61
+ : []
62
+ return {
63
+ ...response,
64
+ history
65
+ }
35
66
  },
36
67
 
37
68
  async setFavorite(options: ClipboardFavoriteOptions): Promise<void> {
@@ -49,8 +80,20 @@ export function useClipboardHistory() {
49
80
  onDidChange(callback: (item: PluginClipboardItem) => void): () => void {
50
81
  return channel.regChannel('core-box:clipboard-change', ({ data }) => {
51
82
  const item = (data && 'item' in data ? data.item : data) as PluginClipboardItem
52
- callback(item)
83
+ callback(normalizeItem(item) ?? item)
53
84
  })
85
+ },
86
+
87
+ /**
88
+ * Writes the provided clipboard payload to the system clipboard and issues a paste command
89
+ * to the foreground application.
90
+ */
91
+ async applyToActiveApp(options: ClipboardApplyOptions = {}): Promise<boolean> {
92
+ const response = await channel.send('clipboard:apply-to-active-app', options)
93
+ if (typeof response === 'object' && response) {
94
+ return Boolean((response as any).success)
95
+ }
96
+ return true
54
97
  }
55
98
  }
56
99
  }
@@ -20,3 +20,4 @@ export * from './service/index'
20
20
  export * from './channel'
21
21
  export * from './clipboard'
22
22
  export * from './storage'
23
+ export * from './system'
@@ -0,0 +1,14 @@
1
+ import type { ActiveAppSnapshot } from './types'
2
+
3
+ const ensurePluginChannel = () => {
4
+ const channel = (window as any)?.$channel
5
+ if (!channel) {
6
+ throw new Error('[Plugin SDK] System channel requires plugin renderer context with $channel available.')
7
+ }
8
+ return channel
9
+ }
10
+
11
+ export async function getActiveAppSnapshot(options: { forceRefresh?: boolean } = {}): Promise<ActiveAppSnapshot | null> {
12
+ const channel = ensurePluginChannel()
13
+ return channel.send('system:get-active-app', options)
14
+ }
@@ -95,6 +95,8 @@ export interface PluginClipboardItem {
95
95
  sourceApp?: string | null
96
96
  timestamp?: string | number | Date | null
97
97
  isFavorite?: boolean | null
98
+ metadata?: string | null
99
+ meta?: Record<string, unknown> | null
98
100
  }
99
101
 
100
102
  /**
@@ -107,6 +109,19 @@ export interface PluginClipboardHistoryResponse {
107
109
  pageSize: number
108
110
  }
109
111
 
112
+ export interface ActiveAppSnapshot {
113
+ identifier: string | null
114
+ displayName: string | null
115
+ bundleId: string | null
116
+ processId: number | null
117
+ executablePath: string | null
118
+ platform: 'macos' | 'windows' | 'linux' | null
119
+ windowTitle: string | null
120
+ url?: string | null
121
+ icon?: string | null
122
+ lastUpdated: number
123
+ }
124
+
110
125
  /**
111
126
  * Plugin utilities interface providing core functionality for plugin development
112
127
  *