@talex-touch/utils 1.0.30 → 1.0.32

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 (113) hide show
  1. package/animation/window-node.ts +205 -0
  2. package/animation/window.ts +19 -15
  3. package/auth/clerk-types.ts +1 -1
  4. package/auth/index.ts +1 -1
  5. package/auth/useAuthState.ts +6 -5
  6. package/auth/useClerkConfig.ts +6 -6
  7. package/auth/useClerkProvider.ts +3 -2
  8. package/channel/index.ts +28 -21
  9. package/common/file-scan-constants.ts +137 -121
  10. package/common/file-scan-utils.ts +49 -25
  11. package/common/index.ts +3 -3
  12. package/common/search/gather.ts +1 -1
  13. package/common/search/index.ts +5 -6
  14. package/common/storage/constants.ts +3 -2
  15. package/common/storage/entity/app-settings.ts +19 -3
  16. package/common/storage/entity/shortcut-settings.ts +10 -10
  17. package/common/storage/shortcut-storage.ts +6 -4
  18. package/common/utils/file.ts +15 -4
  19. package/common/utils/index.ts +62 -52
  20. package/common/utils/polling.ts +114 -63
  21. package/common/utils/task-queue.ts +11 -10
  22. package/common/utils/time.ts +50 -47
  23. package/common/utils/timing.ts +41 -37
  24. package/core-box/builder/index.ts +1 -1
  25. package/core-box/builder/tuff-builder.ts +255 -230
  26. package/core-box/index.ts +3 -6
  27. package/core-box/preview/index.ts +1 -0
  28. package/core-box/preview/types.ts +43 -0
  29. package/core-box/tuff/index.ts +1 -1
  30. package/core-box/tuff/tuff-dsl.ts +419 -253
  31. package/electron/clipboard-helper.ts +20 -12
  32. package/electron/download-manager.ts +43 -42
  33. package/electron/env-tool.ts +19 -18
  34. package/electron/file-parsers/index.ts +2 -2
  35. package/electron/file-parsers/parsers/text-parser.ts +15 -14
  36. package/electron/file-parsers/registry.ts +9 -7
  37. package/electron/file-parsers/types.ts +4 -4
  38. package/electron/index.ts +1 -1
  39. package/eventbus/index.ts +11 -11
  40. package/index.ts +6 -5
  41. package/intelligence/client.ts +87 -0
  42. package/intelligence/index.ts +1 -0
  43. package/package.json +14 -14
  44. package/permission/index.ts +8 -8
  45. package/plugin/channel.ts +77 -68
  46. package/plugin/index.ts +113 -84
  47. package/plugin/install.ts +8 -8
  48. package/plugin/log/types.ts +5 -5
  49. package/plugin/node/index.ts +1 -1
  50. package/plugin/node/logger-manager.ts +14 -11
  51. package/plugin/node/logger.ts +8 -8
  52. package/plugin/plugin-source.ts +11 -11
  53. package/plugin/preload.ts +6 -3
  54. package/plugin/providers/registry.ts +8 -7
  55. package/plugin/providers/types.ts +6 -6
  56. package/plugin/sdk/channel.ts +20 -20
  57. package/plugin/sdk/clipboard.ts +8 -6
  58. package/plugin/sdk/common.ts +10 -6
  59. package/plugin/sdk/core-box.ts +2 -3
  60. package/plugin/sdk/division-box.ts +266 -0
  61. package/plugin/sdk/enum/bridge-event.ts +1 -1
  62. package/plugin/sdk/examples/storage-onDidChange-example.js +1 -1
  63. package/plugin/sdk/features.ts +34 -26
  64. package/plugin/sdk/hooks/bridge.ts +3 -6
  65. package/plugin/sdk/hooks/index.ts +1 -1
  66. package/plugin/sdk/hooks/life-cycle.ts +4 -10
  67. package/plugin/sdk/index.ts +9 -13
  68. package/plugin/sdk/service/index.ts +3 -3
  69. package/plugin/sdk/storage.ts +4 -4
  70. package/plugin/sdk/system.ts +1 -1
  71. package/plugin/sdk/types.ts +169 -143
  72. package/plugin/sdk/window/index.ts +8 -5
  73. package/preload/loading.ts +6 -6
  74. package/preload/renderer.ts +4 -2
  75. package/renderer/hooks/arg-mapper.ts +1 -2
  76. package/renderer/hooks/index.ts +2 -0
  77. package/renderer/hooks/initialize.ts +10 -8
  78. package/renderer/hooks/performance.ts +4 -4
  79. package/renderer/hooks/use-channel.ts +150 -0
  80. package/renderer/hooks/use-intelligence.ts +236 -0
  81. package/renderer/index.ts +6 -1
  82. package/renderer/ref.ts +32 -36
  83. package/renderer/slots.ts +29 -26
  84. package/renderer/storage/app-settings.ts +16 -6
  85. package/renderer/storage/base-storage.ts +236 -88
  86. package/renderer/storage/index.ts +3 -0
  87. package/renderer/storage/intelligence-storage.ts +215 -0
  88. package/renderer/storage/openers.ts +13 -3
  89. package/renderer/touch-sdk/env.ts +41 -41
  90. package/renderer/touch-sdk/index.ts +1 -1
  91. package/renderer/touch-sdk/terminal.ts +5 -5
  92. package/renderer/touch-sdk/utils.ts +4 -3
  93. package/search/levenshtein-utils.ts +11 -11
  94. package/search/types.ts +102 -103
  95. package/service/index.ts +11 -11
  96. package/service/protocol/index.ts +217 -14
  97. package/types/division-box.ts +248 -0
  98. package/types/download.ts +72 -34
  99. package/types/icon.ts +2 -1
  100. package/types/index.ts +3 -1
  101. package/types/intelligence.ts +413 -0
  102. package/types/modules/base.ts +16 -16
  103. package/types/modules/index.ts +1 -1
  104. package/types/modules/module-lifecycle.ts +21 -21
  105. package/types/modules/module-manager.ts +11 -11
  106. package/types/modules/module.ts +16 -16
  107. package/types/storage.ts +0 -1
  108. package/types/touch-app-core.ts +32 -32
  109. package/types/update.ts +79 -21
  110. package/core-box/README.md +0 -218
  111. package/core-box/builder/tuff-builder.example.ts.bak +0 -258
  112. package/core-box/run-tests.sh +0 -7
  113. package/core-box/search.ts +0 -1
@@ -1,4 +1,4 @@
1
- import { ISearchProvider, TuffQuery, TuffSearchResult } from '@talex-touch/utils'
1
+ import type { ISearchProvider, TuffQuery, TuffSearchResult } from '@talex-touch/utils'
2
2
 
3
3
  export * from './gather'
4
4
 
@@ -30,7 +30,6 @@ export interface TuffUpdate {
30
30
  sourceStats?: TuffSearchResult['sources']
31
31
  }
32
32
 
33
-
34
33
  /**
35
34
  * Search Engine Interface (formerly ISearchEngine)
36
35
  *
@@ -41,13 +40,13 @@ export interface ISearchEngine<C> {
41
40
  * Registers a search provider with the engine.
42
41
  * @param provider - An instance of ISearchProvider.
43
42
  */
44
- registerProvider(provider: ISearchProvider<C>): void
43
+ registerProvider: (provider: ISearchProvider<C>) => void
45
44
 
46
45
  /**
47
46
  * Unregisters a search provider by its unique ID.
48
47
  * @param providerId - The unique ID of the provider to remove.
49
48
  */
50
- unregisterProvider(providerId: string): void
49
+ unregisterProvider: (providerId: string) => void
51
50
 
52
51
  /**
53
52
  * Executes a search across all registered and relevant providers.
@@ -57,11 +56,11 @@ export interface ISearchEngine<C> {
57
56
  * @returns A promise that resolves to a TuffSearchResult object,
58
57
  * containing the ranked items and metadata about the search operation.
59
58
  */
60
- search(query: TuffQuery): Promise<TuffSearchResult>
59
+ search: (query: TuffQuery) => Promise<TuffSearchResult>
61
60
 
62
61
  /**
63
62
  * Performs background maintenance tasks, such as pre-heating caches,
64
63
  * refreshing indexes, etc.
65
64
  */
66
- maintain(): void
65
+ maintain: () => void
67
66
  }
@@ -1,7 +1,8 @@
1
1
  export enum StorageList {
2
2
  APP_SETTING = 'app-setting.ini',
3
3
  SHORTCUT_SETTING = 'shortcut-setting.ini',
4
- OPENERS = 'openers.json'
4
+ OPENERS = 'openers.json',
5
+ IntelligenceConfig = 'aisdk-config',
5
6
  }
6
7
 
7
8
  /**
@@ -13,5 +14,5 @@ export enum ConfigKeys {
13
14
  * Stores the timestamp of the last successful full application scan using mdfind.
14
15
  * This is used to schedule the next comprehensive scan.
15
16
  */
16
- APP_PROVIDER_LAST_MDFIND_SCAN = 'app_provider_last_mdfind_scan'
17
+ APP_PROVIDER_LAST_MDFIND_SCAN = 'app_provider_last_mdfind_scan',
17
18
  }
@@ -39,7 +39,23 @@ const _appSettingOriginData = {
39
39
  searchEngine: {
40
40
  logsEnabled: false,
41
41
  },
42
- };
42
+ window: {
43
+ closeToTray: true,
44
+ startMinimized: false,
45
+ startSilent: false,
46
+ },
47
+ setup: {
48
+ accessibility: false,
49
+ notifications: false,
50
+ autoStart: false,
51
+ showTray: true,
52
+ adminPrivileges: false,
53
+ hideDock: false,
54
+ runAsAdmin: false,
55
+ customDesktop: false,
56
+ },
57
+ layout: 'simple',
58
+ }
43
59
 
44
60
  export const appSettingOriginData = Object.freeze(_appSettingOriginData)
45
61
 
@@ -49,5 +65,5 @@ export const appSettingOriginData = Object.freeze(_appSettingOriginData)
49
65
  * Combines the default configuration with support for dynamic additional properties.
50
66
  */
51
67
  export type AppSetting = typeof _appSettingOriginData & {
52
- [key: string]: any;
53
- };
68
+ [key: string]: any
69
+ }
@@ -4,19 +4,19 @@ export enum ShortcutType {
4
4
  }
5
5
 
6
6
  export interface ShortcutMeta {
7
- creationTime: number;
8
- modificationTime: number;
9
- author: string;
10
- description?: string;
7
+ creationTime: number
8
+ modificationTime: number
9
+ author: string
10
+ description?: string
11
11
  }
12
12
 
13
13
  export interface Shortcut {
14
- id: string;
15
- accelerator: string;
16
- type: ShortcutType;
17
- meta: ShortcutMeta;
14
+ id: string
15
+ accelerator: string
16
+ type: ShortcutType
17
+ meta: ShortcutMeta
18
18
  }
19
19
 
20
- export type ShortcutSetting = Shortcut[];
20
+ export type ShortcutSetting = Shortcut[]
21
21
 
22
- export const shortcutSettingOriginData: ShortcutSetting = [];
22
+ export const shortcutSettingOriginData: ShortcutSetting = []
@@ -1,12 +1,13 @@
1
+ import type { Shortcut, ShortcutSetting } from './entity/shortcut-settings'
1
2
  import { StorageList } from './constants'
2
- import { shortcutSettingOriginData, ShortcutSetting, Shortcut } from './entity/shortcut-settings'
3
+ import { shortcutSettingOriginData } from './entity/shortcut-settings'
3
4
 
4
5
  class ShortcutStorage {
5
6
  private _config: ShortcutSetting = []
6
7
 
7
8
  constructor(private readonly storage: {
8
- getConfig: (name: string) => any,
9
- saveConfig: (name: string, content?: string) => void,
9
+ getConfig: (name: string) => any
10
+ saveConfig: (name: string, content?: string) => void
10
11
  }) {
11
12
  this.init()
12
13
  }
@@ -16,7 +17,8 @@ class ShortcutStorage {
16
17
  if (!config || !Array.isArray(config) || config.length === 0) {
17
18
  this._config = [...shortcutSettingOriginData]
18
19
  this._save()
19
- } else {
20
+ }
21
+ else {
20
22
  this._config = config
21
23
  }
22
24
  }
@@ -1,4 +1,15 @@
1
- import path from 'path-browserify'
1
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
2
+ const path = (() => {
3
+ if (typeof window === 'undefined') {
4
+ return require('node:path')
5
+ }
6
+ try {
7
+ return require('path-browserify')
8
+ }
9
+ catch {
10
+ return require('node:path')
11
+ }
12
+ })()
2
13
 
3
14
  /**
4
15
  * Enum for various file types.
@@ -18,7 +29,7 @@ export enum FileType {
18
29
  Spreadsheet = 'Spreadsheet',
19
30
  Presentation = 'Presentation',
20
31
  Ebook = 'Ebook',
21
- Other = 'Other'
32
+ Other = 'Other',
22
33
  }
23
34
 
24
35
  const extensionMap: Map<FileType, Set<string>> = new Map([
@@ -34,8 +45,8 @@ const extensionMap: Map<FileType, Set<string>> = new Map([
34
45
  [FileType.Font, new Set(['.ttf', '.otf', '.woff', '.woff2'])],
35
46
  [FileType.Spreadsheet, new Set(['.xls', '.xlsx', '.csv', '.numbers'])],
36
47
  [FileType.Presentation, new Set(['.ppt', '.pptx', '.key'])],
37
- [FileType.Ebook, new Set(['.epub', '.mobi', '.azw'])]
38
- ]);
48
+ [FileType.Ebook, new Set(['.epub', '.mobi', '.azw'])],
49
+ ])
39
50
 
40
51
  /**
41
52
  * Get the file type from a file path.
@@ -10,7 +10,7 @@
10
10
  * ```
11
11
  */
12
12
  export async function sleep(time: number): Promise<number> {
13
- return new Promise(resolve => setTimeout(() => resolve(time), time))
13
+ return new Promise(resolve => setTimeout(() => resolve(time), time))
14
14
  }
15
15
 
16
16
  /**
@@ -28,14 +28,14 @@ export async function sleep(time: number): Promise<number> {
28
28
  * ```
29
29
  */
30
30
  export function anyStr2Num(str: string): bigint {
31
- const codes = Array.from(str).map(ch => ch.charCodeAt(0))
32
- const minCode = Math.min(...codes)
31
+ const codes = Array.from(str).map(ch => ch.charCodeAt(0))
32
+ const minCode = Math.min(...codes)
33
33
 
34
- const encoded = codes
35
- .map(code => (code - minCode).toString().padStart(2, '0'))
36
- .join('')
34
+ const encoded = codes
35
+ .map(code => (code - minCode).toString().padStart(2, '0'))
36
+ .join('')
37
37
 
38
- return BigInt(`${minCode}000${encoded}`)
38
+ return BigInt(`${minCode}000${encoded}`)
39
39
  }
40
40
 
41
41
  /**
@@ -51,16 +51,16 @@ export function anyStr2Num(str: string): bigint {
51
51
  * ```
52
52
  */
53
53
  export function num2anyStr(num: bigint): string {
54
- const [baseStr, encoded] = num.toString().split('000')
55
- const base = Number(baseStr)
54
+ const [baseStr, encoded] = num.toString().split('000')
55
+ const base = Number(baseStr)
56
56
 
57
- let result = ''
58
- for (let i = 0; i < encoded.length; i += 2) {
59
- const offset = Number(encoded.slice(i, i + 2))
60
- result += String.fromCharCode(base + offset)
61
- }
57
+ let result = ''
58
+ for (let i = 0; i < encoded.length; i += 2) {
59
+ const offset = Number(encoded.slice(i, i + 2))
60
+ result += String.fromCharCode(base + offset)
61
+ }
62
62
 
63
- return result
63
+ return result
64
64
  }
65
65
 
66
66
  const LOCALHOST_KEYS = ['localhost', '127.0.0.1']
@@ -78,7 +78,7 @@ const LOCALHOST_KEYS = ['localhost', '127.0.0.1']
78
78
  * ```
79
79
  */
80
80
  export function isLocalhostUrl(urlStr: string): boolean {
81
- return LOCALHOST_KEYS.includes(new URL(urlStr).hostname)
81
+ return LOCALHOST_KEYS.includes(new URL(urlStr).hostname)
82
82
  }
83
83
 
84
84
  /**
@@ -90,85 +90,95 @@ export function isLocalhostUrl(urlStr: string): boolean {
90
90
  * @throws Error if an unsupported value is found (reports path and type).
91
91
  */
92
92
  export function structuredStrictStringify(value: unknown): string {
93
- const seen = new WeakMap<object, string>();
93
+ const seen = new WeakMap<object, string>()
94
94
  const badTypes = [
95
- 'symbol'
96
- ];
95
+ 'symbol',
96
+ ]
97
97
  // Support Map/Set, but not Error, DOM, Proxy, WeakMap, WeakSet
98
98
  function getType(val: any): string {
99
- if (val === null) return 'null';
100
- if (Array.isArray(val)) return 'Array';
99
+ if (val === null)
100
+ return 'null'
101
+ if (Array.isArray(val))
102
+ return 'Array'
101
103
  if (typeof Document !== 'undefined') {
102
- if (val instanceof Node) return 'DOMNode';
104
+ if (val instanceof Node)
105
+ return 'DOMNode'
103
106
  }
104
107
  // if (val instanceof Error) return 'Error';
105
- if (val instanceof WeakMap) return 'WeakMap';
106
- if (val instanceof WeakSet) return 'WeakSet';
107
- if (typeof val === 'object' && val !== null && val.constructor?.name === 'Proxy') return 'Proxy';
108
- if (typeof val === 'bigint') return 'BigInt';
109
- return typeof val;
108
+ if (val instanceof WeakMap)
109
+ return 'WeakMap'
110
+ if (val instanceof WeakSet)
111
+ return 'WeakSet'
112
+ if (typeof val === 'object' && val !== null && val.constructor?.name === 'Proxy')
113
+ return 'Proxy'
114
+ if (typeof val === 'bigint')
115
+ return 'BigInt'
116
+ return typeof val
110
117
  }
111
118
 
112
119
  function serialize(val: any, path: string): any {
113
- const type = getType(val);
120
+ const type = getType(val)
114
121
  // Block disallowed/unsafe types and edge cases for structured-clone
115
122
  if (badTypes.includes(typeof val) || type === 'DOMNode' || type === 'Proxy' || type === 'WeakMap' || type === 'WeakSet' || type === 'BigInt') {
116
- throw new Error(`Cannot serialize property at path "${path}": type "${type}"`);
123
+ throw new Error(`Cannot serialize property at path "${path}": type "${type}"`)
117
124
  }
118
125
  // JSON doesn't support undefined, skip it for values in objects, preserve in arrays as null
119
- if (typeof val === 'undefined') return null;
126
+ if (typeof val === 'undefined')
127
+ return null
120
128
  // Simple value
121
129
  if (
122
- val === null ||
123
- typeof val === 'number' ||
124
- typeof val === 'boolean' ||
125
- typeof val === 'string'
126
- ) return val;
130
+ val === null
131
+ || typeof val === 'number'
132
+ || typeof val === 'boolean'
133
+ || typeof val === 'string'
134
+ ) {
135
+ return val
136
+ }
127
137
  // Cycle check
128
138
  if (typeof val === 'object') {
129
139
  if (seen.has(val)) {
130
- return `[Circular ~${seen.get(val)}]`; // You could just throw if you dislike this fallback!
140
+ return `[Circular ~${seen.get(val)}]` // You could just throw if you dislike this fallback!
131
141
  }
132
- seen.set(val, path);
142
+ seen.set(val, path)
133
143
  if (val instanceof Error) {
134
144
  return {
135
145
  name: val.name,
136
146
  message: val.message,
137
147
  stack: val.stack,
138
- };
148
+ }
139
149
  }
140
150
  if (Array.isArray(val)) {
141
- return val.map((item, idx) => serialize(item, `${path}[${idx}]`));
151
+ return val.map((item, idx) => serialize(item, `${path}[${idx}]`))
142
152
  }
143
153
  if (val instanceof Date) {
144
- return val.toISOString();
154
+ return val.toISOString()
145
155
  }
146
156
  if (val instanceof Map) {
147
- const obj: Record<string, any> = {};
157
+ const obj: Record<string, any> = {}
148
158
  for (const [k, v] of val.entries()) {
149
- obj[typeof k === 'string' ? k : JSON.stringify(k)] = serialize(v, `${path}[Map(${typeof k === 'string' ? k : JSON.stringify(k)})]`);
159
+ obj[typeof k === 'string' ? k : JSON.stringify(k)] = serialize(v, `${path}[Map(${typeof k === 'string' ? k : JSON.stringify(k)})]`)
150
160
  }
151
- return obj;
161
+ return obj
152
162
  }
153
163
  if (val instanceof Set) {
154
- return Array.from(val).map((item, idx) => serialize(item, `${path}[SetEntry${idx}]`));
164
+ return Array.from(val).map((item, idx) => serialize(item, `${path}[SetEntry${idx}]`))
155
165
  }
156
166
  // General object
157
- const res: any = {};
167
+ const res: any = {}
158
168
  for (const key of Object.keys(val)) {
159
- res[key] = serialize(val[key], `${path}.${key}`);
169
+ res[key] = serialize(val[key], `${path}.${key}`)
160
170
  }
161
- return res;
171
+ return res
162
172
  }
163
- throw new Error(`Cannot serialize property at path "${path}": unknown type`);
173
+ throw new Error(`Cannot serialize property at path "${path}": unknown type`)
164
174
  }
165
175
 
166
- return JSON.stringify(serialize(value, 'root'));
176
+ return JSON.stringify(serialize(value, 'root'))
167
177
  }
168
178
 
169
- export * from './timing'
179
+ export * from './file'
170
180
 
171
- export { runAdaptiveTaskQueue, type AdaptiveTaskQueueOptions } from './task-queue'
181
+ export { type AdaptiveTaskQueueOptions, runAdaptiveTaskQueue } from './task-queue'
172
182
 
173
183
  export * from './time'
174
- export * from './file'
184
+ export * from './timing'
@@ -6,19 +6,20 @@
6
6
  */
7
7
 
8
8
  interface PollingTask {
9
- id: string;
10
- callback: () => void | Promise<void>;
11
- intervalMs: number;
12
- nextRunMs: number;
9
+ id: string
10
+ callback: () => void | Promise<void>
11
+ intervalMs: number
12
+ nextRunMs: number
13
13
  }
14
14
 
15
- type TimeUnit = 'seconds' | 'minutes' | 'hours';
15
+ type TimeUnit = 'seconds' | 'minutes' | 'hours'
16
16
 
17
17
  export class PollingService {
18
- private static instance: PollingService;
19
- private tasks = new Map<string, PollingTask>();
20
- private timerId: NodeJS.Timeout | null = null;
21
- private isRunning = false;
18
+ private static instance: PollingService
19
+ private tasks = new Map<string, PollingTask>()
20
+ private timerId: NodeJS.Timeout | null = null
21
+ private isRunning = false
22
+ private quitListenerCleanup?: () => void
22
23
 
23
24
  private constructor() {
24
25
  // Private constructor to enforce singleton pattern
@@ -26,21 +27,21 @@ export class PollingService {
26
27
 
27
28
  public static getInstance(): PollingService {
28
29
  if (!PollingService.instance) {
29
- PollingService.instance = new PollingService();
30
+ PollingService.instance = new PollingService()
30
31
  }
31
- return PollingService.instance;
32
+ return PollingService.instance
32
33
  }
33
34
 
34
35
  private convertToMs(interval: number, unit: TimeUnit): number {
35
36
  switch (unit) {
36
37
  case 'seconds':
37
- return interval * 1000;
38
+ return interval * 1000
38
39
  case 'minutes':
39
- return interval * 60 * 1000;
40
+ return interval * 60 * 1000
40
41
  case 'hours':
41
- return interval * 60 * 60 * 1000;
42
+ return interval * 60 * 60 * 1000
42
43
  default:
43
- throw new Error(`Invalid time unit: ${unit}`);
44
+ throw new Error(`Invalid time unit: ${unit}`)
44
45
  }
45
46
  }
46
47
 
@@ -53,31 +54,31 @@ export class PollingService {
53
54
  public register(
54
55
  id: string,
55
56
  callback: () => void | Promise<void>,
56
- options: { interval: number; unit: TimeUnit; runImmediately?: boolean }
57
+ options: { interval: number, unit: TimeUnit, runImmediately?: boolean },
57
58
  ): void {
58
59
  if (this.tasks.has(id)) {
59
- console.warn(`[PollingService] Task with ID '${id}' is already registered. Overwriting.`);
60
+ console.warn(`[PollingService] Task with ID '${id}' is already registered. Overwriting.`)
60
61
  }
61
62
 
62
- const intervalMs = this.convertToMs(options.interval, options.unit);
63
+ const intervalMs = this.convertToMs(options.interval, options.unit)
63
64
  if (intervalMs <= 0) {
64
- console.error(`[PollingService] Task '${id}' has an invalid interval of ${intervalMs}ms. Registration aborted.`);
65
- return;
65
+ console.error(`[PollingService] Task '${id}' has an invalid interval of ${intervalMs}ms. Registration aborted.`)
66
+ return
66
67
  }
67
68
 
68
- const nextRunMs = options.runImmediately ? Date.now() : Date.now() + intervalMs;
69
+ const nextRunMs = options.runImmediately ? Date.now() : Date.now() + intervalMs
69
70
 
70
71
  this.tasks.set(id, {
71
72
  id,
72
73
  callback,
73
74
  intervalMs,
74
75
  nextRunMs,
75
- });
76
+ })
76
77
 
77
- console.log(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`);
78
+ console.log(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`)
78
79
 
79
80
  if (this.isRunning) {
80
- this._reschedule();
81
+ this._reschedule()
81
82
  }
82
83
  }
83
84
 
@@ -87,12 +88,13 @@ export class PollingService {
87
88
  */
88
89
  public unregister(id: string): void {
89
90
  if (this.tasks.delete(id)) {
90
- console.log(`[PollingService] Task '${id}' unregistered.`);
91
+ console.log(`[PollingService] Task '${id}' unregistered.`)
91
92
  if (this.isRunning) {
92
- this._reschedule();
93
+ this._reschedule()
93
94
  }
94
- } else {
95
- console.warn(`[PollingService] Attempted to unregister a non-existent task with ID '${id}'.`);
95
+ }
96
+ else {
97
+ console.warn(`[PollingService] Attempted to unregister a non-existent task with ID '${id}'.`)
96
98
  }
97
99
  }
98
100
 
@@ -102,7 +104,7 @@ export class PollingService {
102
104
  * @returns - True if the task is registered, false otherwise.
103
105
  */
104
106
  public isRegistered(id: string): boolean {
105
- return this.tasks.has(id);
107
+ return this.tasks.has(id)
106
108
  }
107
109
 
108
110
  /**
@@ -111,74 +113,123 @@ export class PollingService {
111
113
  */
112
114
  public start(): void {
113
115
  if (this.isRunning) {
114
- return;
116
+ console.warn('[PollingService] Already running, skipping start.')
117
+ return
118
+ }
119
+ this.isRunning = true
120
+ console.log('[PollingService] Polling service started')
121
+ this._setupQuitListener()
122
+ this._reschedule()
123
+ }
124
+
125
+ /**
126
+ * Sets up Electron app quit listener if running in Electron environment
127
+ * Uses lazy resolution to avoid hard dependency on Electron
128
+ */
129
+ private _setupQuitListener(): void {
130
+ // Check if we're in Electron environment
131
+ try {
132
+ // Use dynamic require to avoid hard dependency on Electron
133
+ // Similar to the approach used in packages/utils/plugin/channel.ts
134
+ const electron = (globalThis as any)?.electron
135
+ ?? (typeof require !== 'undefined' ? require('electron') : null)
136
+
137
+ if (electron?.app) {
138
+ const app = electron.app
139
+
140
+ // Listen to before-quit event
141
+ const quitHandler = () => {
142
+ this.stop('app quit')
143
+ }
144
+
145
+ app.on('before-quit', quitHandler)
146
+
147
+ // Store cleanup function
148
+ this.quitListenerCleanup = () => {
149
+ app.removeListener('before-quit', quitHandler)
150
+ }
151
+ }
152
+ }
153
+ catch (error) {
154
+ // Not in Electron environment or Electron not available
155
+ // This is fine, just skip the quit listener setup
115
156
  }
116
- this.isRunning = true;
117
- console.debug('[PollingService] Service started.');
118
- this._reschedule();
119
157
  }
120
158
 
121
159
  /**
122
160
  * Stops the polling service and clears all scheduled tasks.
161
+ * @param reason - Optional reason for stopping the service (for logging purposes)
123
162
  */
124
- public stop(): void {
163
+ public stop(reason?: string): void {
125
164
  if (!this.isRunning) {
126
- return;
165
+ return
127
166
  }
128
- this.isRunning = false;
167
+ this.isRunning = false
129
168
  if (this.timerId) {
130
- clearTimeout(this.timerId);
131
- this.timerId = null;
169
+ clearTimeout(this.timerId)
170
+ this.timerId = null
171
+ }
172
+
173
+ // Clean up quit listener
174
+ if (this.quitListenerCleanup) {
175
+ this.quitListenerCleanup()
176
+ this.quitListenerCleanup = undefined
177
+ }
178
+
179
+ if (reason) {
180
+ console.log(`[PollingService] Stopping polling service: ${reason}`)
181
+ }
182
+ else {
183
+ console.log('[PollingService] Polling service stopped')
132
184
  }
133
- console.log('[PollingService] Service stopped.');
134
185
  }
135
186
 
136
187
  private _reschedule(): void {
137
188
  if (this.timerId) {
138
- clearTimeout(this.timerId);
139
- this.timerId = null;
189
+ clearTimeout(this.timerId)
190
+ this.timerId = null
140
191
  }
141
192
 
142
193
  if (!this.isRunning || this.tasks.size === 0) {
143
- return;
194
+ return
144
195
  }
145
196
 
146
- const now = Date.now();
197
+ const now = Date.now()
147
198
  const nextTask = Array.from(this.tasks.values()).reduce((prev, curr) =>
148
- prev.nextRunMs < curr.nextRunMs ? prev : curr
149
- );
199
+ prev.nextRunMs < curr.nextRunMs ? prev : curr,
200
+ )
150
201
 
151
- const delay = Math.max(0, nextTask.nextRunMs - now);
152
- this.timerId = setTimeout(() => this._tick(), delay);
202
+ const delay = Math.max(0, nextTask.nextRunMs - now)
203
+ this.timerId = setTimeout(() => this._tick(), delay)
153
204
  }
154
205
 
155
206
  private async _tick(): Promise<void> {
156
- const now = Date.now();
157
- const tasksToRun: PollingTask[] = [];
207
+ const now = Date.now()
208
+ const tasksToRun: PollingTask[] = []
158
209
 
159
210
  for (const task of this.tasks.values()) {
160
211
  if (task.nextRunMs <= now) {
161
- tasksToRun.push(task);
212
+ tasksToRun.push(task)
162
213
  }
163
214
  }
164
215
 
165
216
  if (tasksToRun.length > 0) {
166
- // console.debug(`[PollingService] Executing ${tasksToRun.length} tasks.`);
167
- for (const task of tasksToRun) {
168
- try {
169
- await task.callback();
170
- } catch (error) {
171
- console.error(`[PollingService] Error executing task '${task.id}':`, error);
172
- }
173
- // Update next run time based on its last scheduled run time, not 'now'.
174
- // This prevents drift if a task takes a long time to execute.
175
- task.nextRunMs += task.intervalMs;
217
+ // console.debug(`[PollingService] Executing ${tasksToRun.length} tasks.`);
218
+ for (const task of tasksToRun) {
219
+ try {
220
+ await task.callback()
221
+ }
222
+ catch (error) {
223
+ console.error(`[PollingService] Error executing task '${task.id}':`, error)
176
224
  }
225
+ // Update next run time based on its last scheduled run time, not 'now'.
226
+ // This prevents drift if a task takes a long time to execute.
227
+ task.nextRunMs += task.intervalMs
228
+ }
177
229
  }
178
230
 
179
-
180
- this._reschedule();
231
+ this._reschedule()
181
232
  }
182
233
  }
183
234
 
184
- export const pollingService = PollingService.getInstance();
235
+ export const pollingService = PollingService.getInstance()