@talex-touch/utils 1.0.18 → 1.0.21
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/channel/index.ts +49 -1
- package/common/index.ts +2 -0
- package/common/search/gather.ts +45 -0
- package/common/search/index.ts +67 -0
- package/common/storage/constants.ts +16 -2
- package/common/storage/entity/index.ts +2 -1
- package/common/storage/entity/openers.ts +32 -0
- package/common/storage/entity/shortcut-settings.ts +22 -0
- package/common/storage/shortcut-storage.ts +58 -0
- package/common/utils/file.ts +62 -0
- package/common/{utils.ts → utils/index.ts} +14 -2
- package/common/utils/polling.ts +184 -0
- package/common/utils/task-queue.ts +108 -0
- package/common/utils/time.ts +374 -0
- package/core-box/README.md +8 -8
- package/core-box/builder/index.ts +6 -0
- package/core-box/builder/tuff-builder.example.ts.bak +258 -0
- package/core-box/builder/tuff-builder.ts +1162 -0
- package/core-box/index.ts +5 -2
- package/core-box/run-tests.sh +7 -0
- package/core-box/search.ts +1 -536
- package/core-box/tuff/index.ts +6 -0
- package/core-box/tuff/tuff-dsl.ts +1412 -0
- package/electron/clipboard-helper.ts +199 -0
- package/electron/env-tool.ts +36 -2
- package/electron/file-parsers/index.ts +8 -0
- package/electron/file-parsers/parsers/text-parser.ts +109 -0
- package/electron/file-parsers/registry.ts +92 -0
- package/electron/file-parsers/types.ts +58 -0
- package/electron/index.ts +3 -0
- package/eventbus/index.ts +0 -7
- package/index.ts +3 -1
- package/package.json +4 -29
- package/plugin/channel.ts +48 -16
- package/plugin/index.ts +194 -30
- package/plugin/log/types.ts +11 -0
- package/plugin/node/index.ts +4 -0
- package/plugin/node/logger-manager.ts +113 -0
- package/plugin/{log → node}/logger.ts +41 -7
- package/plugin/plugin-source.ts +74 -0
- package/plugin/preload.ts +5 -15
- package/plugin/providers/index.ts +2 -0
- package/plugin/providers/registry.ts +47 -0
- package/plugin/providers/types.ts +54 -0
- package/plugin/risk/index.ts +1 -0
- package/plugin/risk/types.ts +20 -0
- package/plugin/sdk/enum/bridge-event.ts +4 -0
- package/plugin/sdk/enum/index.ts +1 -0
- package/plugin/sdk/hooks/bridge.ts +68 -0
- package/plugin/sdk/hooks/index.ts +2 -1
- package/plugin/sdk/hooks/life-cycle.ts +2 -4
- package/plugin/sdk/index.ts +2 -0
- package/plugin/sdk/storage.ts +84 -0
- package/plugin/sdk/types.ts +2 -2
- package/plugin/sdk/window/index.ts +5 -3
- package/preload/index.ts +2 -0
- package/preload/loading.ts +15 -0
- package/preload/renderer.ts +41 -0
- package/renderer/hooks/arg-mapper.ts +79 -0
- package/renderer/hooks/index.ts +2 -0
- package/renderer/hooks/initialize.ts +198 -0
- package/renderer/index.ts +3 -0
- package/renderer/storage/app-settings.ts +2 -0
- package/renderer/storage/base-storage.ts +1 -0
- package/renderer/storage/openers.ts +11 -0
- package/renderer/touch-sdk/env.ts +106 -0
- package/renderer/touch-sdk/index.ts +108 -0
- package/renderer/touch-sdk/terminal.ts +85 -0
- package/renderer/touch-sdk/utils.ts +61 -0
- package/search/levenshtein-utils.ts +39 -0
- package/search/types.ts +16 -16
- package/types/index.ts +2 -1
- package/types/modules/base.ts +146 -0
- package/types/modules/index.ts +4 -0
- package/types/modules/module-lifecycle.ts +148 -0
- package/types/modules/module-manager.ts +99 -0
- package/types/modules/module.ts +112 -0
- package/types/touch-app-core.ts +16 -93
- package/core-box/types.ts +0 -384
- package/plugin/log/logger-manager.ts +0 -60
package/channel/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ITouchChannel {
|
|
|
21
21
|
regChannel(type: ChannelType, eventName: string, callback: (data: StandardChannelData) => any): () => void
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
+
* @deprecated Use sendMain instead
|
|
24
25
|
* Send a message to a channel
|
|
25
26
|
* @param type {@link ChannelType} The type of channel
|
|
26
27
|
* @param eventName {string} The name of event, must be unique in the channel {@link ChannelType}
|
|
@@ -29,6 +30,7 @@ export interface ITouchChannel {
|
|
|
29
30
|
send(type: ChannelType, eventName: string, arg?: any): Promise<any>
|
|
30
31
|
|
|
31
32
|
/**
|
|
33
|
+
* @deprecated Use sendToMain instead
|
|
32
34
|
* Send a message to a channel with settled window
|
|
33
35
|
* @param win {@link Electron.BrowserWindow} the window you want to sent
|
|
34
36
|
* @param type {@link ChannelType} The type of channel
|
|
@@ -36,6 +38,51 @@ export interface ITouchChannel {
|
|
|
36
38
|
* @param arg {any} The arguments of the message
|
|
37
39
|
*/
|
|
38
40
|
sendTo(win: Electron.BrowserWindow, type: ChannelType, eventName: string, arg: any): Promise<any>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Send a message to main process
|
|
44
|
+
* @param eventName {string} The name of event, must be unique in the channel {@link ChannelType}
|
|
45
|
+
* @param arg {any} The arguments of the message
|
|
46
|
+
*/
|
|
47
|
+
sendMain(eventName: string, arg?: any): Promise<any>
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Send a message to main process with settled window
|
|
51
|
+
* @param win {@link Electron.BrowserWindow} the window you want to sent
|
|
52
|
+
* @param eventName {string} The name of event, must be unique in the channel {@link ChannelType}
|
|
53
|
+
* @param arg {any} The arguments of the message
|
|
54
|
+
*/
|
|
55
|
+
sendToMain(win: Electron.BrowserWindow, eventName: string, arg?: any): Promise<any>
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Send a message to all plugin process with settled window
|
|
59
|
+
* @param eventName {string} The name of event, must be unique in the channel {@link ChannelType}
|
|
60
|
+
* @param arg {any} The arguments of the message
|
|
61
|
+
*/
|
|
62
|
+
sendPlugin(pluginName: string, eventName: string, arg?: any): Promise<any>
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send a message to plugin process with settled window
|
|
66
|
+
* @param pluginName {string} The name of plugin
|
|
67
|
+
* @param eventName {string} The name of event, must be unique in the channel {@link ChannelType}
|
|
68
|
+
* @param arg {any} The arguments of the message
|
|
69
|
+
*/
|
|
70
|
+
sendToPlugin(pluginName: string, eventName: string, arg?: any): Promise<any>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Request a encrypted name key. This key cannot decrypted to get the original name.
|
|
74
|
+
* After use, you should revoke this key.
|
|
75
|
+
* @description Request a encrypted name key, and return the encrypted key
|
|
76
|
+
* @param name {string} The name of key
|
|
77
|
+
*/
|
|
78
|
+
requestKey: (name: string) => string
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Unregister a encrypted name key
|
|
82
|
+
* @description Unregister a encrypted name key, and return the encrypted key
|
|
83
|
+
* @param key {string} The encrypted key
|
|
84
|
+
*/
|
|
85
|
+
revokeKey: (key: string) => boolean
|
|
39
86
|
}
|
|
40
87
|
|
|
41
88
|
export interface ITouchClientChannel {
|
|
@@ -84,6 +131,7 @@ export interface RawChannelHeaderData {
|
|
|
84
131
|
status: "reply" | "request";
|
|
85
132
|
type: ChannelType;
|
|
86
133
|
_originData?: any;
|
|
134
|
+
uniqueKey?: string
|
|
87
135
|
event?: Electron.IpcMainEvent | Electron.IpcRendererEvent;
|
|
88
136
|
}
|
|
89
137
|
|
|
@@ -105,4 +153,4 @@ export interface StandardChannelData extends RawStandardChannelData {
|
|
|
105
153
|
|
|
106
154
|
export type IChannelData = any //boolean | number | string | null | undefined | {
|
|
107
155
|
// [prop: string]: any
|
|
108
|
-
// }
|
|
156
|
+
// }
|
package/common/index.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TuffUpdate } from "."
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @interface ITuffGatherOptions
|
|
5
|
+
* @description Configuration options for the search result aggregator.
|
|
6
|
+
*/
|
|
7
|
+
export interface ITuffGatherOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The number of providers to run in parallel.
|
|
10
|
+
* @default 3
|
|
11
|
+
*/
|
|
12
|
+
concurrency?: number
|
|
13
|
+
/**
|
|
14
|
+
* The time to wait for more results before flushing the buffer.
|
|
15
|
+
* @default 100
|
|
16
|
+
*/
|
|
17
|
+
coalesceGapMs?: number
|
|
18
|
+
/**
|
|
19
|
+
* A shorter grace period for the first batch to ensure a quick initial response.
|
|
20
|
+
* @default 50
|
|
21
|
+
*/
|
|
22
|
+
firstBatchGraceMs?: number
|
|
23
|
+
/**
|
|
24
|
+
* A small debounce delay for the push function to avoid rapid-fire updates.
|
|
25
|
+
* @default 10
|
|
26
|
+
*/
|
|
27
|
+
debouncePushMs?: number
|
|
28
|
+
/**
|
|
29
|
+
* The maximum time to wait for a single provider to return results.
|
|
30
|
+
* @default 5000
|
|
31
|
+
*/
|
|
32
|
+
taskTimeoutMs?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Defines the type signature for the real-time update callback function.
|
|
37
|
+
* @param update - The data object containing update information.
|
|
38
|
+
*/
|
|
39
|
+
export type TuffAggregatorCallback = (update: TuffUpdate) => void
|
|
40
|
+
|
|
41
|
+
export interface IGatherController {
|
|
42
|
+
abort: () => void
|
|
43
|
+
promise: Promise<number>
|
|
44
|
+
signal: AbortSignal
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ISearchProvider, TuffQuery, TuffSearchResult } from '@talex-touch/utils'
|
|
2
|
+
|
|
3
|
+
export * from './gather'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a single update pushed from the search-gatherer.
|
|
7
|
+
* It provides a snapshot of the search progress at a point in time.
|
|
8
|
+
*/
|
|
9
|
+
export interface TuffUpdate {
|
|
10
|
+
/**
|
|
11
|
+
* New search results from the current push batch.
|
|
12
|
+
* Each element is a complete TuffSearchResult from a provider.
|
|
13
|
+
*/
|
|
14
|
+
newResults: TuffSearchResult[]
|
|
15
|
+
/**
|
|
16
|
+
* Total number of items aggregated so far.
|
|
17
|
+
*/
|
|
18
|
+
totalCount: number
|
|
19
|
+
/**
|
|
20
|
+
* Flag indicating whether all search tasks (both default and fallback queues) have completed.
|
|
21
|
+
*/
|
|
22
|
+
isDone: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Flag indicating whether the search was cancelled.
|
|
25
|
+
*/
|
|
26
|
+
cancelled?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Statistics about the performance of each search provider.
|
|
29
|
+
*/
|
|
30
|
+
sourceStats?: TuffSearchResult['sources']
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Search Engine Interface (formerly ISearchEngine)
|
|
36
|
+
*
|
|
37
|
+
* Defines the core functionality of the search aggregator and orchestrator.
|
|
38
|
+
*/
|
|
39
|
+
export interface ISearchEngine<C> {
|
|
40
|
+
/**
|
|
41
|
+
* Registers a search provider with the engine.
|
|
42
|
+
* @param provider - An instance of ISearchProvider.
|
|
43
|
+
*/
|
|
44
|
+
registerProvider(provider: ISearchProvider<C>): void
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unregisters a search provider by its unique ID.
|
|
48
|
+
* @param providerId - The unique ID of the provider to remove.
|
|
49
|
+
*/
|
|
50
|
+
unregisterProvider(providerId: string): void
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Executes a search across all registered and relevant providers.
|
|
54
|
+
* It aggregates, scores, and ranks the results.
|
|
55
|
+
*
|
|
56
|
+
* @param query - The search query object.
|
|
57
|
+
* @returns A promise that resolves to a TuffSearchResult object,
|
|
58
|
+
* containing the ranked items and metadata about the search operation.
|
|
59
|
+
*/
|
|
60
|
+
search(query: TuffQuery): Promise<TuffSearchResult>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Performs background maintenance tasks, such as pre-heating caches,
|
|
64
|
+
* refreshing indexes, etc.
|
|
65
|
+
*/
|
|
66
|
+
maintain(): void
|
|
67
|
+
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
export enum StorageList {
|
|
2
|
-
APP_SETTING =
|
|
3
|
-
|
|
2
|
+
APP_SETTING = 'app-setting.ini',
|
|
3
|
+
SHORTCUT_SETTING = 'shortcut-setting.ini',
|
|
4
|
+
OPENERS = 'openers.json'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines keys for the global configuration stored in the database `config` table.
|
|
9
|
+
* Using an enum prevents magic strings and ensures consistency across the application.
|
|
10
|
+
*/
|
|
11
|
+
export enum ConfigKeys {
|
|
12
|
+
/**
|
|
13
|
+
* Stores the timestamp of the last successful full application scan using mdfind.
|
|
14
|
+
* This is used to schedule the next comprehensive scan.
|
|
15
|
+
*/
|
|
16
|
+
APP_PROVIDER_LAST_MDFIND_SCAN = 'app_provider_last_mdfind_scan'
|
|
17
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from './app-settings'
|
|
1
|
+
export * from './app-settings'
|
|
2
|
+
export * from './openers'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface OpenerInfo {
|
|
2
|
+
/**
|
|
3
|
+
* Bundle identifier of the application responsible for handling the file type.
|
|
4
|
+
*/
|
|
5
|
+
bundleId: string
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Display name of the application.
|
|
9
|
+
*/
|
|
10
|
+
name: string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Icon for the application (data URL or resolvable path).
|
|
14
|
+
*/
|
|
15
|
+
logo: string
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Absolute path of the application bundle/executable.
|
|
19
|
+
*/
|
|
20
|
+
path?: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ISO timestamp representing when this mapping was last refreshed.
|
|
24
|
+
*/
|
|
25
|
+
lastResolvedAt?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type OpenersMap = Record<string, OpenerInfo>
|
|
29
|
+
|
|
30
|
+
const _openersOriginData: OpenersMap = {}
|
|
31
|
+
|
|
32
|
+
export const openersOriginData = Object.freeze(_openersOriginData)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export enum ShortcutType {
|
|
2
|
+
MAIN = 'main',
|
|
3
|
+
RENDERER = 'renderer',
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ShortcutMeta {
|
|
7
|
+
creationTime: number;
|
|
8
|
+
modificationTime: number;
|
|
9
|
+
author: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Shortcut {
|
|
14
|
+
id: string;
|
|
15
|
+
accelerator: string;
|
|
16
|
+
type: ShortcutType;
|
|
17
|
+
meta: ShortcutMeta;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ShortcutSetting = Shortcut[];
|
|
21
|
+
|
|
22
|
+
export const shortcutSettingOriginData: ShortcutSetting = [];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { StorageList } from './constants'
|
|
2
|
+
import { shortcutSettingOriginData, ShortcutSetting, Shortcut } from './entity/shortcut-settings'
|
|
3
|
+
|
|
4
|
+
class ShortcutStorage {
|
|
5
|
+
private _config: ShortcutSetting = []
|
|
6
|
+
|
|
7
|
+
constructor(private readonly storage: {
|
|
8
|
+
getConfig: (name: string) => any,
|
|
9
|
+
saveConfig: (name: string, content?: string) => void,
|
|
10
|
+
}) {
|
|
11
|
+
this.init()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private init() {
|
|
15
|
+
const config = this.storage.getConfig(StorageList.SHORTCUT_SETTING)
|
|
16
|
+
if (!config || !Array.isArray(config) || config.length === 0) {
|
|
17
|
+
this._config = [...shortcutSettingOriginData]
|
|
18
|
+
this._save()
|
|
19
|
+
} else {
|
|
20
|
+
this._config = config
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private _save() {
|
|
25
|
+
this.storage.saveConfig(StorageList.SHORTCUT_SETTING, JSON.stringify(this._config, null, 2))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getAllShortcuts(): Shortcut[] {
|
|
29
|
+
return this._config
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getShortcutById(id: string): Shortcut | undefined {
|
|
33
|
+
return this._config.find(s => s.id === id)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
addShortcut(shortcut: Shortcut): boolean {
|
|
37
|
+
if (this.getShortcutById(shortcut.id)) {
|
|
38
|
+
console.warn(`Shortcut with ID ${shortcut.id} already exists.`)
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
this._config.push(shortcut)
|
|
42
|
+
this._save()
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
updateShortcutAccelerator(id: string, newAccelerator: string): boolean {
|
|
47
|
+
const shortcut = this.getShortcutById(id)
|
|
48
|
+
if (!shortcut) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
shortcut.accelerator = newAccelerator
|
|
52
|
+
shortcut.meta.modificationTime = Date.now()
|
|
53
|
+
this._save()
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default ShortcutStorage
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from 'path-browserify'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enum for various file types.
|
|
5
|
+
* @enum {string}
|
|
6
|
+
*/
|
|
7
|
+
export enum FileType {
|
|
8
|
+
Image = 'Image',
|
|
9
|
+
Document = 'Document',
|
|
10
|
+
Audio = 'Audio',
|
|
11
|
+
Video = 'Video',
|
|
12
|
+
Archive = 'Archive',
|
|
13
|
+
Code = 'Code',
|
|
14
|
+
Text = 'Text',
|
|
15
|
+
Design = 'Design',
|
|
16
|
+
Model3D = '3D Model',
|
|
17
|
+
Font = 'Font',
|
|
18
|
+
Spreadsheet = 'Spreadsheet',
|
|
19
|
+
Presentation = 'Presentation',
|
|
20
|
+
Ebook = 'Ebook',
|
|
21
|
+
Other = 'Other'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const extensionMap: Map<FileType, Set<string>> = new Map([
|
|
25
|
+
[FileType.Image, new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.tiff'])],
|
|
26
|
+
[FileType.Document, new Set(['.doc', '.docx', '.pdf', '.odt', '.rtf'])],
|
|
27
|
+
[FileType.Audio, new Set(['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a'])],
|
|
28
|
+
[FileType.Video, new Set(['.mp4', '.avi', '.mov', '.wmv', '.mkv', '.flv', '.webm'])],
|
|
29
|
+
[FileType.Archive, new Set(['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'])],
|
|
30
|
+
[FileType.Code, new Set(['.js', '.ts', '.jsx', '.tsx', '.html', '.css', '.scss', '.json', '.xml', '.java', '.py', '.c', '.cpp', '.go', '.rs', '.php', '.sh'])],
|
|
31
|
+
[FileType.Text, new Set(['.txt', '.md', '.log'])],
|
|
32
|
+
[FileType.Design, new Set(['.psd', '.ai', '.fig', '.sketch', '.xd', '.afdesign'])],
|
|
33
|
+
[FileType.Model3D, new Set(['.obj', '.fbx', '.stl', '.dae', '.blend', '.3ds'])],
|
|
34
|
+
[FileType.Font, new Set(['.ttf', '.otf', '.woff', '.woff2'])],
|
|
35
|
+
[FileType.Spreadsheet, new Set(['.xls', '.xlsx', '.csv', '.numbers'])],
|
|
36
|
+
[FileType.Presentation, new Set(['.ppt', '.pptx', '.key'])],
|
|
37
|
+
[FileType.Ebook, new Set(['.epub', '.mobi', '.azw'])]
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the file type from a file path.
|
|
42
|
+
* @param {string} filePath - The path to the file.
|
|
43
|
+
* @returns {FileType} The type of the file.
|
|
44
|
+
*/
|
|
45
|
+
export function getFileTypeFromPath(filePath: string): FileType {
|
|
46
|
+
const extension = path.extname(filePath).toLowerCase()
|
|
47
|
+
return getFileTypeFromExtension(extension)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the file type from a file extension.
|
|
52
|
+
* @param {string} extension - The file extension, including the dot.
|
|
53
|
+
* @returns {FileType} The type of the file.
|
|
54
|
+
*/
|
|
55
|
+
export function getFileTypeFromExtension(extension: string): FileType {
|
|
56
|
+
for (const [type, extensions] of extensionMap.entries()) {
|
|
57
|
+
if (extensions.has(extension)) {
|
|
58
|
+
return type
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return FileType.Other
|
|
62
|
+
}
|
|
@@ -101,7 +101,7 @@ export function structuredStrictStringify(value: unknown): string {
|
|
|
101
101
|
if (typeof Document !== 'undefined') {
|
|
102
102
|
if (val instanceof Node) return 'DOMNode';
|
|
103
103
|
}
|
|
104
|
-
if (val instanceof Error) return 'Error';
|
|
104
|
+
// if (val instanceof Error) return 'Error';
|
|
105
105
|
if (val instanceof WeakMap) return 'WeakMap';
|
|
106
106
|
if (val instanceof WeakSet) return 'WeakSet';
|
|
107
107
|
if (typeof val === 'object' && val !== null && val.constructor?.name === 'Proxy') return 'Proxy';
|
|
@@ -112,7 +112,7 @@ export function structuredStrictStringify(value: unknown): string {
|
|
|
112
112
|
function serialize(val: any, path: string): any {
|
|
113
113
|
const type = getType(val);
|
|
114
114
|
// Block disallowed/unsafe types and edge cases for structured-clone
|
|
115
|
-
if (badTypes.includes(typeof val) || type === 'DOMNode' || type === '
|
|
115
|
+
if (badTypes.includes(typeof val) || type === 'DOMNode' || type === 'Proxy' || type === 'WeakMap' || type === 'WeakSet' || type === 'BigInt') {
|
|
116
116
|
throw new Error(`Cannot serialize property at path "${path}": type "${type}"`);
|
|
117
117
|
}
|
|
118
118
|
// JSON doesn't support undefined, skip it for values in objects, preserve in arrays as null
|
|
@@ -130,6 +130,13 @@ export function structuredStrictStringify(value: unknown): string {
|
|
|
130
130
|
return `[Circular ~${seen.get(val)}]`; // You could just throw if you dislike this fallback!
|
|
131
131
|
}
|
|
132
132
|
seen.set(val, path);
|
|
133
|
+
if (val instanceof Error) {
|
|
134
|
+
return {
|
|
135
|
+
name: val.name,
|
|
136
|
+
message: val.message,
|
|
137
|
+
stack: val.stack,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
133
140
|
if (Array.isArray(val)) {
|
|
134
141
|
return val.map((item, idx) => serialize(item, `${path}[${idx}]`));
|
|
135
142
|
}
|
|
@@ -158,3 +165,8 @@ export function structuredStrictStringify(value: unknown): string {
|
|
|
158
165
|
|
|
159
166
|
return JSON.stringify(serialize(value, 'root'));
|
|
160
167
|
}
|
|
168
|
+
|
|
169
|
+
export { runAdaptiveTaskQueue, type AdaptiveTaskQueueOptions } from './task-queue'
|
|
170
|
+
|
|
171
|
+
export * from './time'
|
|
172
|
+
export * from './file'
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module polling
|
|
3
|
+
* A high-precision, efficient, singleton polling service for scheduling periodic tasks.
|
|
4
|
+
* This service uses a dynamic setTimeout approach, ensuring tasks are executed
|
|
5
|
+
* with high accuracy while minimizing CPU usage and battery consumption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface PollingTask {
|
|
9
|
+
id: string;
|
|
10
|
+
callback: () => void | Promise<void>;
|
|
11
|
+
intervalMs: number;
|
|
12
|
+
nextRunMs: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type TimeUnit = 'seconds' | 'minutes' | 'hours';
|
|
16
|
+
|
|
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;
|
|
22
|
+
|
|
23
|
+
private constructor() {
|
|
24
|
+
// Private constructor to enforce singleton pattern
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static getInstance(): PollingService {
|
|
28
|
+
if (!PollingService.instance) {
|
|
29
|
+
PollingService.instance = new PollingService();
|
|
30
|
+
}
|
|
31
|
+
return PollingService.instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private convertToMs(interval: number, unit: TimeUnit): number {
|
|
35
|
+
switch (unit) {
|
|
36
|
+
case 'seconds':
|
|
37
|
+
return interval * 1000;
|
|
38
|
+
case 'minutes':
|
|
39
|
+
return interval * 60 * 1000;
|
|
40
|
+
case 'hours':
|
|
41
|
+
return interval * 60 * 60 * 1000;
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Invalid time unit: ${unit}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registers a new periodic task with the service.
|
|
49
|
+
* @param id - A unique identifier for the task.
|
|
50
|
+
* @param callback - The function to be executed.
|
|
51
|
+
* @param options - The execution interval.
|
|
52
|
+
*/
|
|
53
|
+
public register(
|
|
54
|
+
id: string,
|
|
55
|
+
callback: () => void | Promise<void>,
|
|
56
|
+
options: { interval: number; unit: TimeUnit; runImmediately?: boolean }
|
|
57
|
+
): void {
|
|
58
|
+
if (this.tasks.has(id)) {
|
|
59
|
+
console.warn(`[PollingService] Task with ID '${id}' is already registered. Overwriting.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const intervalMs = this.convertToMs(options.interval, options.unit);
|
|
63
|
+
if (intervalMs <= 0) {
|
|
64
|
+
console.error(`[PollingService] Task '${id}' has an invalid interval of ${intervalMs}ms. Registration aborted.`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nextRunMs = options.runImmediately ? Date.now() : Date.now() + intervalMs;
|
|
69
|
+
|
|
70
|
+
this.tasks.set(id, {
|
|
71
|
+
id,
|
|
72
|
+
callback,
|
|
73
|
+
intervalMs,
|
|
74
|
+
nextRunMs,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`);
|
|
78
|
+
|
|
79
|
+
if (this.isRunning) {
|
|
80
|
+
this._reschedule();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unregisters a task, preventing it from being executed in the future.
|
|
86
|
+
* @param id - The unique identifier of the task to remove.
|
|
87
|
+
*/
|
|
88
|
+
public unregister(id: string): void {
|
|
89
|
+
if (this.tasks.delete(id)) {
|
|
90
|
+
console.log(`[PollingService] Task '${id}' unregistered.`);
|
|
91
|
+
if (this.isRunning) {
|
|
92
|
+
this._reschedule();
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
console.warn(`[PollingService] Attempted to unregister a non-existent task with ID '${id}'.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks if a task is already registered.
|
|
101
|
+
* @param id - The unique identifier of the task.
|
|
102
|
+
* @returns - True if the task is registered, false otherwise.
|
|
103
|
+
*/
|
|
104
|
+
public isRegistered(id: string): boolean {
|
|
105
|
+
return this.tasks.has(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Starts the polling service.
|
|
110
|
+
* It's safe to call this method multiple times.
|
|
111
|
+
*/
|
|
112
|
+
public start(): void {
|
|
113
|
+
if (this.isRunning) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.isRunning = true;
|
|
117
|
+
console.log('[PollingService] Service started.');
|
|
118
|
+
this._reschedule();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Stops the polling service and clears all scheduled tasks.
|
|
123
|
+
*/
|
|
124
|
+
public stop(): void {
|
|
125
|
+
if (!this.isRunning) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.isRunning = false;
|
|
129
|
+
if (this.timerId) {
|
|
130
|
+
clearTimeout(this.timerId);
|
|
131
|
+
this.timerId = null;
|
|
132
|
+
}
|
|
133
|
+
console.log('[PollingService] Service stopped.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private _reschedule(): void {
|
|
137
|
+
if (this.timerId) {
|
|
138
|
+
clearTimeout(this.timerId);
|
|
139
|
+
this.timerId = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!this.isRunning || this.tasks.size === 0) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const nextTask = Array.from(this.tasks.values()).reduce((prev, curr) =>
|
|
148
|
+
prev.nextRunMs < curr.nextRunMs ? prev : curr
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const delay = Math.max(0, nextTask.nextRunMs - now);
|
|
152
|
+
this.timerId = setTimeout(() => this._tick(), delay);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async _tick(): Promise<void> {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const tasksToRun: PollingTask[] = [];
|
|
158
|
+
|
|
159
|
+
for (const task of this.tasks.values()) {
|
|
160
|
+
if (task.nextRunMs <= now) {
|
|
161
|
+
tasksToRun.push(task);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
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;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
this._reschedule();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const pollingService = PollingService.getInstance();
|