@talex-touch/utils 1.0.31 → 1.0.33
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/animation/window-node.ts +15 -12
- package/animation/window.ts +19 -15
- package/auth/clerk-types.ts +1 -1
- package/auth/index.ts +1 -1
- package/auth/useAuthState.ts +6 -5
- package/auth/useClerkConfig.ts +4 -4
- package/auth/useClerkProvider.ts +3 -2
- package/channel/index.ts +23 -22
- package/common/file-scan-constants.ts +137 -121
- package/common/file-scan-utils.ts +48 -27
- package/common/index.ts +3 -3
- package/common/search/gather.ts +1 -1
- package/common/search/index.ts +5 -6
- package/common/storage/constants.ts +3 -2
- package/common/storage/entity/app-settings.ts +5 -3
- package/common/storage/entity/shortcut-settings.ts +10 -10
- package/common/storage/shortcut-storage.ts +6 -4
- package/common/utils/file.ts +14 -6
- package/common/utils/index.ts +62 -52
- package/common/utils/polling.ts +88 -84
- package/common/utils/task-queue.ts +11 -10
- package/common/utils/time.ts +50 -47
- package/common/utils/timing.ts +41 -37
- package/core-box/builder/index.ts +1 -1
- package/core-box/builder/tuff-builder.ts +254 -229
- package/core-box/index.ts +4 -6
- package/core-box/preview/index.ts +1 -0
- package/core-box/preview/types.ts +43 -0
- package/core-box/recommendation.ts +77 -0
- package/core-box/tuff/index.ts +1 -1
- package/core-box/tuff/tuff-dsl.ts +328 -266
- package/electron/download-manager.ts +43 -42
- package/electron/env-tool.ts +19 -18
- package/electron/file-parsers/index.ts +2 -2
- package/electron/file-parsers/parsers/text-parser.ts +15 -14
- package/electron/file-parsers/registry.ts +9 -7
- package/electron/file-parsers/types.ts +4 -4
- package/electron/index.ts +2 -2
- package/eventbus/index.ts +11 -11
- package/index.ts +5 -4
- package/intelligence/client.ts +87 -0
- package/intelligence/index.ts +1 -0
- package/package.json +14 -14
- package/permission/index.ts +8 -8
- package/plugin/channel.ts +77 -68
- package/plugin/index.ts +96 -82
- package/plugin/install.ts +8 -8
- package/plugin/log/types.ts +5 -5
- package/plugin/node/index.ts +1 -1
- package/plugin/node/logger-manager.ts +14 -11
- package/plugin/node/logger.ts +8 -8
- package/plugin/plugin-source.ts +11 -11
- package/plugin/preload.ts +1 -1
- package/plugin/providers/registry.ts +8 -7
- package/plugin/providers/types.ts +6 -6
- package/plugin/sdk/README.md +216 -0
- package/plugin/sdk/box-sdk.ts +219 -0
- package/plugin/sdk/channel.ts +20 -20
- package/plugin/sdk/clipboard.ts +8 -6
- package/plugin/sdk/common.ts +10 -6
- package/plugin/sdk/core-box.ts +2 -3
- package/plugin/sdk/division-box.ts +266 -0
- package/plugin/sdk/enum/bridge-event.ts +1 -1
- package/plugin/sdk/examples/storage-onDidChange-example.js +1 -1
- package/plugin/sdk/feature-sdk.ts +235 -0
- package/plugin/sdk/features.ts +34 -26
- package/plugin/sdk/hooks/bridge.ts +3 -6
- package/plugin/sdk/hooks/index.ts +1 -1
- package/plugin/sdk/hooks/life-cycle.ts +4 -10
- package/plugin/sdk/index.ts +10 -7
- package/plugin/sdk/service/index.ts +3 -3
- package/plugin/sdk/storage.ts +4 -4
- package/plugin/sdk/system.ts +1 -1
- package/plugin/sdk/types.ts +165 -146
- package/plugin/sdk/window/index.ts +8 -5
- package/preload/loading.ts +6 -6
- package/preload/renderer.ts +4 -2
- package/renderer/hooks/arg-mapper.ts +1 -2
- package/renderer/hooks/index.ts +2 -0
- package/renderer/hooks/initialize.ts +10 -8
- package/renderer/hooks/performance.ts +4 -4
- package/renderer/hooks/use-channel.ts +150 -0
- package/renderer/hooks/use-intelligence.ts +236 -0
- package/renderer/index.ts +6 -2
- package/renderer/ref.ts +32 -36
- package/renderer/slots.ts +29 -26
- package/renderer/storage/app-settings.ts +16 -6
- package/renderer/storage/base-storage.ts +222 -114
- package/renderer/storage/index.ts +3 -0
- package/renderer/storage/intelligence-storage.ts +218 -0
- package/renderer/storage/openers.ts +13 -3
- package/renderer/touch-sdk/env.ts +41 -41
- package/renderer/touch-sdk/index.ts +1 -1
- package/renderer/touch-sdk/terminal.ts +5 -5
- package/renderer/touch-sdk/utils.ts +4 -3
- package/search/levenshtein-utils.ts +11 -11
- package/search/types.ts +102 -102
- package/service/index.ts +11 -11
- package/service/protocol/index.ts +217 -14
- package/types/division-box.ts +248 -0
- package/types/download.ts +72 -34
- package/types/index.ts +3 -1
- package/types/intelligence.ts +607 -0
- package/types/modules/base.ts +16 -16
- package/types/modules/index.ts +1 -1
- package/types/modules/module-lifecycle.ts +21 -21
- package/types/modules/module-manager.ts +11 -11
- package/types/modules/module.ts +16 -16
- package/types/storage.ts +0 -1
- package/types/touch-app-core.ts +32 -32
- package/types/update.ts +91 -21
- package/core-box/README.md +0 -218
- package/core-box/builder/tuff-builder.example.ts.bak +0 -258
- package/core-box/run-tests.sh +0 -7
- package/core-box/search.ts +0 -1
- package/electron/clipboard-helper.ts +0 -199
package/renderer/slots.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { VNode } from 'vue'
|
|
1
|
+
import type { VNode } from 'vue'
|
|
2
2
|
|
|
3
|
-
type SlotSelector = string | string[] | ((name: string) => boolean)
|
|
4
|
-
type VNodePredicate = (node: VNode) => boolean
|
|
3
|
+
type SlotSelector = string | string[] | ((name: string) => boolean)
|
|
4
|
+
type VNodePredicate = (node: VNode) => boolean
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Normalizes a slot input into a flat array of VNodes.
|
|
@@ -16,17 +16,18 @@ type VNodePredicate = (node: VNode) => boolean;
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
export function normalizeSlot(input: unknown): VNode[] {
|
|
19
|
-
if (!input)
|
|
19
|
+
if (!input)
|
|
20
|
+
return []
|
|
20
21
|
|
|
21
22
|
if (typeof input === 'function') {
|
|
22
|
-
return normalizeSlot(input())
|
|
23
|
+
return normalizeSlot(input())
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
if (Array.isArray(input)) {
|
|
26
|
-
return input.flatMap(normalizeSlot)
|
|
27
|
+
return input.flatMap(normalizeSlot)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
return [input as VNode]
|
|
30
|
+
return [input as VNode]
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -44,22 +45,24 @@ export function normalizeSlot(input: unknown): VNode[] {
|
|
|
44
45
|
*/
|
|
45
46
|
export function flattenVNodes(
|
|
46
47
|
nodes: VNode[],
|
|
47
|
-
predicate?: VNodePredicate
|
|
48
|
+
predicate?: VNodePredicate,
|
|
48
49
|
): VNode[] {
|
|
49
|
-
const result: VNode[] = []
|
|
50
|
+
const result: VNode[] = []
|
|
50
51
|
|
|
51
52
|
for (const node of nodes) {
|
|
52
|
-
if (!node)
|
|
53
|
+
if (!node)
|
|
54
|
+
continue
|
|
53
55
|
|
|
54
56
|
if (predicate?.(node)) {
|
|
55
|
-
result.push(node)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
result.push(node)
|
|
58
|
+
}
|
|
59
|
+
else if (node.children) {
|
|
60
|
+
const children = normalizeSlot(node.children)
|
|
61
|
+
result.push(...flattenVNodes(children, predicate))
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
return result
|
|
65
|
+
return result
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/**
|
|
@@ -86,15 +89,15 @@ export function flattenVNodes(
|
|
|
86
89
|
export function extractFromSlots(
|
|
87
90
|
slots: Record<string, unknown>,
|
|
88
91
|
slotSelector: SlotSelector = 'default',
|
|
89
|
-
predicate?: VNodePredicate
|
|
92
|
+
predicate?: VNodePredicate,
|
|
90
93
|
): VNode[] {
|
|
91
|
-
const selectedSlotNames = resolveSlotNames(slots, slotSelector)
|
|
94
|
+
const selectedSlotNames = resolveSlotNames(slots, slotSelector)
|
|
92
95
|
|
|
93
|
-
const vnodes = selectedSlotNames.flatMap(
|
|
94
|
-
normalizeSlot(slots[name])
|
|
95
|
-
)
|
|
96
|
+
const vnodes = selectedSlotNames.flatMap(name =>
|
|
97
|
+
normalizeSlot(slots[name]),
|
|
98
|
+
)
|
|
96
99
|
|
|
97
|
-
return flattenVNodes(vnodes, predicate)
|
|
100
|
+
return flattenVNodes(vnodes, predicate)
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
|
@@ -106,19 +109,19 @@ export function extractFromSlots(
|
|
|
106
109
|
*/
|
|
107
110
|
function resolveSlotNames(
|
|
108
111
|
slots: Record<string, unknown>,
|
|
109
|
-
selector: SlotSelector
|
|
112
|
+
selector: SlotSelector,
|
|
110
113
|
): string[] {
|
|
111
114
|
if (typeof selector === 'string') {
|
|
112
|
-
return slots[selector] ? [selector] : []
|
|
115
|
+
return slots[selector] ? [selector] : []
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
if (Array.isArray(selector)) {
|
|
116
|
-
return selector.filter(
|
|
119
|
+
return selector.filter(name => !!slots[name])
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
if (typeof selector === 'function') {
|
|
120
|
-
return Object.keys(slots).filter(selector)
|
|
123
|
+
return Object.keys(slots).filter(selector)
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
return []
|
|
126
|
+
return []
|
|
124
127
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { appSettingOriginData, StorageList
|
|
1
|
+
import type { AppSetting } from '../..'
|
|
2
|
+
import { appSettingOriginData, StorageList } from '../..'
|
|
3
|
+
import { createStorageProxy, TouchStorage } from './base-storage'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Application settings storage manager
|
|
@@ -11,7 +12,7 @@ import { appSettingOriginData, StorageList, type AppSetting } from '../..';
|
|
|
11
12
|
* ```ts
|
|
12
13
|
* import { appSettings } from './app-settings-storage';
|
|
13
14
|
*
|
|
14
|
-
* //
|
|
15
|
+
* // Access after initStorageChannel()
|
|
15
16
|
* const isAutoStart = appSettings.data.autoStart;
|
|
16
17
|
*
|
|
17
18
|
* // Modify a setting (auto-saved)
|
|
@@ -23,12 +24,21 @@ class AppSettingsStorage extends TouchStorage<AppSetting> {
|
|
|
23
24
|
* Initializes a new instance of the AppSettingsStorage class
|
|
24
25
|
*/
|
|
25
26
|
constructor() {
|
|
26
|
-
super(StorageList.APP_SETTING, JSON.parse(JSON.stringify(appSettingOriginData)))
|
|
27
|
-
this.setAutoSave(true)
|
|
27
|
+
super(StorageList.APP_SETTING, JSON.parse(JSON.stringify(appSettingOriginData)))
|
|
28
|
+
this.setAutoSave(true)
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Global instance of the application settings
|
|
33
34
|
*/
|
|
34
|
-
|
|
35
|
+
const APP_SETTINGS_SINGLETON_KEY = `storage:${StorageList.APP_SETTING}`
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Lazy-initialized application settings.
|
|
39
|
+
* The actual instance is created only when first accessed AND after initStorageChannel() is called.
|
|
40
|
+
*/
|
|
41
|
+
export const appSettings = createStorageProxy<AppSettingsStorage>(
|
|
42
|
+
APP_SETTINGS_SINGLETON_KEY,
|
|
43
|
+
() => new AppSettingsStorage(),
|
|
44
|
+
)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { UnwrapNestedRefs, WatchHandle } from 'vue'
|
|
2
|
+
import type { ITouchClientChannel } from '../../channel'
|
|
3
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
1
4
|
import {
|
|
2
5
|
reactive,
|
|
6
|
+
|
|
3
7
|
watch,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from 'vue';
|
|
7
|
-
import { useDebounceFn } from '@vueuse/core'
|
|
8
|
-
import type { ITouchClientChannel } from '../../channel';
|
|
8
|
+
|
|
9
|
+
} from 'vue'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Interface representing the external communication channel.
|
|
@@ -17,26 +18,21 @@ export interface IStorageChannel extends ITouchClientChannel {
|
|
|
17
18
|
* @param event Event name
|
|
18
19
|
* @param payload Event payload
|
|
19
20
|
*/
|
|
20
|
-
send(event: string, payload: unknown)
|
|
21
|
+
send: (event: string, payload: unknown) => Promise<unknown>
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Synchronous send interface
|
|
24
25
|
* @param event Event name
|
|
25
26
|
* @param payload Event payload
|
|
26
27
|
*/
|
|
27
|
-
sendSync(event: string, payload: unknown)
|
|
28
|
+
sendSync: (event: string, payload: unknown) => unknown
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
let channel: IStorageChannel | null = null
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Queue of initialization callbacks waiting for channel initialization
|
|
34
|
-
*/
|
|
35
|
-
const pendingInitializations: Array<() => void> = [];
|
|
31
|
+
let channel: IStorageChannel | null = null
|
|
36
32
|
|
|
37
33
|
/**
|
|
38
34
|
* Initializes the global channel for communication.
|
|
39
|
-
*
|
|
35
|
+
* Must be called before creating any TouchStorage instances.
|
|
40
36
|
*
|
|
41
37
|
* @example
|
|
42
38
|
* ```ts
|
|
@@ -50,21 +46,72 @@ const pendingInitializations: Array<() => void> = [];
|
|
|
50
46
|
* ```
|
|
51
47
|
*/
|
|
52
48
|
export function initStorageChannel(c: IStorageChannel): void {
|
|
53
|
-
channel = c
|
|
49
|
+
channel = c
|
|
50
|
+
}
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Global registry of storage instances.
|
|
54
|
+
*/
|
|
55
|
+
const GLOBAL_STORAGE_MAP_KEY = '__talex_touch_storages__'
|
|
56
|
+
|
|
57
|
+
type GlobalStorageMap = Map<string, TouchStorage<any>>
|
|
58
|
+
|
|
59
|
+
function getGlobalStorageMap(): GlobalStorageMap {
|
|
60
|
+
const globalObj = globalThis as typeof globalThis & {
|
|
61
|
+
[GLOBAL_STORAGE_MAP_KEY]?: GlobalStorageMap
|
|
58
62
|
}
|
|
63
|
+
if (!globalObj[GLOBAL_STORAGE_MAP_KEY]) {
|
|
64
|
+
globalObj[GLOBAL_STORAGE_MAP_KEY] = new Map<string, TouchStorage<any>>()
|
|
65
|
+
}
|
|
66
|
+
return globalObj[GLOBAL_STORAGE_MAP_KEY]!
|
|
67
|
+
}
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
export const storages: GlobalStorageMap = getGlobalStorageMap()
|
|
70
|
+
|
|
71
|
+
const GLOBAL_SINGLETON_KEY = '__talex_touch_storage_singletons__'
|
|
72
|
+
type StorageSingletonMap = Map<string, unknown>
|
|
73
|
+
|
|
74
|
+
function getSingletonMap(): StorageSingletonMap {
|
|
75
|
+
const globalObj = globalThis as typeof globalThis & {
|
|
76
|
+
[GLOBAL_SINGLETON_KEY]?: StorageSingletonMap
|
|
77
|
+
}
|
|
78
|
+
if (!globalObj[GLOBAL_SINGLETON_KEY]) {
|
|
79
|
+
globalObj[GLOBAL_SINGLETON_KEY] = new Map<string, unknown>()
|
|
80
|
+
}
|
|
81
|
+
return globalObj[GLOBAL_SINGLETON_KEY]!
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
/**
|
|
65
|
-
*
|
|
85
|
+
* Retrieves an existing storage singleton registered on the global scope,
|
|
86
|
+
* or creates it lazily when missing. Useful to avoid duplicate TouchStorage
|
|
87
|
+
* instantiations under HMR or multi-renderer scenarios.
|
|
66
88
|
*/
|
|
67
|
-
export
|
|
89
|
+
export function getOrCreateStorageSingleton<T>(key: string, factory: () => T): T {
|
|
90
|
+
const map = getSingletonMap()
|
|
91
|
+
if (map.has(key)) {
|
|
92
|
+
return map.get(key) as T
|
|
93
|
+
}
|
|
94
|
+
const instance = factory()
|
|
95
|
+
map.set(key, instance)
|
|
96
|
+
return instance
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates a proxy that lazily initializes a singleton storage instance and
|
|
101
|
+
* ensures all method calls are bound to the real instance so private fields
|
|
102
|
+
* stay accessible (Proxy `this` would otherwise break private member access).
|
|
103
|
+
*/
|
|
104
|
+
export function createStorageProxy<T extends object>(key: string, factory: () => T): T {
|
|
105
|
+
return new Proxy({} as T, {
|
|
106
|
+
get(_target, prop) {
|
|
107
|
+
const instance = getOrCreateStorageSingleton(key, factory)
|
|
108
|
+
const property = (instance as Record<PropertyKey, unknown>)[prop as PropertyKey]
|
|
109
|
+
return typeof property === 'function'
|
|
110
|
+
? property.bind(instance)
|
|
111
|
+
: property
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
}
|
|
68
115
|
|
|
69
116
|
/**
|
|
70
117
|
* A reactive storage utility with optional auto-save and update subscriptions.
|
|
@@ -72,82 +119,94 @@ export const storages = new Map<string, TouchStorage<any>>();
|
|
|
72
119
|
* @template T Shape of the stored data.
|
|
73
120
|
*/
|
|
74
121
|
export class TouchStorage<T extends object> {
|
|
75
|
-
readonly #qualifiedName: string
|
|
76
|
-
#autoSave = false
|
|
77
|
-
#autoSaveStopHandle?: WatchHandle
|
|
78
|
-
#assigning = false
|
|
79
|
-
readonly originalData: T
|
|
80
|
-
private readonly _onUpdate: Array<() => void> = []
|
|
81
|
-
#channelInitialized = false
|
|
122
|
+
readonly #qualifiedName: string
|
|
123
|
+
#autoSave = false
|
|
124
|
+
#autoSaveStopHandle?: WatchHandle
|
|
125
|
+
#assigning = false
|
|
126
|
+
readonly originalData: T
|
|
127
|
+
private readonly _onUpdate: Array<() => void> = []
|
|
128
|
+
#channelInitialized = false
|
|
129
|
+
#skipNextWatchTrigger = false
|
|
82
130
|
|
|
83
131
|
/**
|
|
84
132
|
* The reactive data exposed to users.
|
|
85
133
|
*/
|
|
86
|
-
public data: UnwrapNestedRefs<T
|
|
134
|
+
public data: UnwrapNestedRefs<T>
|
|
87
135
|
|
|
88
136
|
/**
|
|
89
137
|
* Creates a new reactive storage instance.
|
|
90
|
-
*
|
|
138
|
+
* IMPORTANT: `initStorageChannel()` must be called before creating any TouchStorage instances.
|
|
91
139
|
*
|
|
92
140
|
* @param qName Globally unique name for the instance
|
|
93
141
|
* @param initData Initial data to populate the storage
|
|
94
142
|
* @param onUpdate Optional callback when data is updated
|
|
95
143
|
*
|
|
144
|
+
* @throws {Error} If channel is not initialized or if storage with same name already exists
|
|
145
|
+
*
|
|
96
146
|
* @example
|
|
97
147
|
* ```ts
|
|
148
|
+
* // First initialize the channel
|
|
149
|
+
* initStorageChannel(touchChannel);
|
|
150
|
+
*
|
|
151
|
+
* // Then create storage instances
|
|
98
152
|
* const settings = new TouchStorage('settings', { darkMode: false });
|
|
99
153
|
* ```
|
|
100
154
|
*/
|
|
101
155
|
constructor(qName: string, initData: T, onUpdate?: () => void) {
|
|
156
|
+
if (!channel) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`TouchStorage: Cannot create storage "${qName}" before channel is initialized. `
|
|
159
|
+
+ 'Please call initStorageChannel() first.',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
102
163
|
if (storages.has(qName)) {
|
|
103
|
-
throw new Error(`Storage "${qName}" already exists`)
|
|
164
|
+
throw new Error(`Storage "${qName}" already exists`)
|
|
104
165
|
}
|
|
105
166
|
|
|
106
|
-
this.#qualifiedName = qName
|
|
107
|
-
this.originalData = initData
|
|
108
|
-
this.data = reactive({ ...initData }) as UnwrapNestedRefs<T
|
|
167
|
+
this.#qualifiedName = qName
|
|
168
|
+
this.originalData = initData
|
|
169
|
+
this.data = reactive({ ...initData }) as UnwrapNestedRefs<T>
|
|
109
170
|
|
|
110
|
-
if (onUpdate)
|
|
171
|
+
if (onUpdate)
|
|
172
|
+
this._onUpdate.push(onUpdate)
|
|
111
173
|
|
|
112
174
|
// Register to storages map immediately
|
|
113
|
-
storages.set(qName, this)
|
|
114
|
-
|
|
115
|
-
// Initialize channel-dependent operations
|
|
116
|
-
|
|
117
|
-
this.#initializeChannel();
|
|
118
|
-
} else {
|
|
119
|
-
// Queue initialization callback for later
|
|
120
|
-
pendingInitializations.push(() => this.#initializeChannel());
|
|
121
|
-
}
|
|
175
|
+
storages.set(qName, this)
|
|
176
|
+
|
|
177
|
+
// Initialize channel-dependent operations immediately
|
|
178
|
+
this.#initializeChannel()
|
|
122
179
|
}
|
|
123
180
|
|
|
124
181
|
/**
|
|
125
|
-
* Initialize channel-dependent operations
|
|
182
|
+
* Initialize channel-dependent operations.
|
|
183
|
+
* Called immediately in constructor after channel validation.
|
|
126
184
|
*/
|
|
127
185
|
#initializeChannel(): void {
|
|
128
186
|
if (this.#channelInitialized) {
|
|
129
|
-
return
|
|
187
|
+
return
|
|
130
188
|
}
|
|
131
189
|
|
|
132
|
-
|
|
133
|
-
throw new Error(
|
|
134
|
-
'TouchStorage: channel is not initialized. Please call initStorageChannel(...) before using.'
|
|
135
|
-
);
|
|
136
|
-
}
|
|
190
|
+
this.#channelInitialized = true
|
|
137
191
|
|
|
138
|
-
|
|
192
|
+
const result = channel!.sendSync('storage:get', this.#qualifiedName)
|
|
193
|
+
const parsed = result ? (result as Partial<T>) : {}
|
|
139
194
|
|
|
140
|
-
|
|
141
|
-
this.loadFromRemote();
|
|
195
|
+
this.assignData(parsed)
|
|
142
196
|
|
|
143
197
|
// Register update listener
|
|
144
|
-
channel
|
|
198
|
+
channel!.regChannel('storage:update', ({ data }) => {
|
|
145
199
|
const { name } = data!
|
|
146
200
|
|
|
147
201
|
if (name === this.#qualifiedName) {
|
|
148
202
|
this.loadFromRemote()
|
|
149
203
|
}
|
|
150
|
-
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Start auto-save watcher AFTER initial data load
|
|
207
|
+
if (this.#autoSave && !this.#autoSaveStopHandle) {
|
|
208
|
+
this.#startAutoSaveWatcher()
|
|
209
|
+
}
|
|
151
210
|
}
|
|
152
211
|
|
|
153
212
|
/**
|
|
@@ -159,7 +218,7 @@ export class TouchStorage<T extends object> {
|
|
|
159
218
|
* ```
|
|
160
219
|
*/
|
|
161
220
|
getQualifiedName(): string {
|
|
162
|
-
return this.#qualifiedName
|
|
221
|
+
return this.#qualifiedName
|
|
163
222
|
}
|
|
164
223
|
|
|
165
224
|
/**
|
|
@@ -171,7 +230,7 @@ export class TouchStorage<T extends object> {
|
|
|
171
230
|
* ```
|
|
172
231
|
*/
|
|
173
232
|
isAutoSave(): boolean {
|
|
174
|
-
return this.#autoSave
|
|
233
|
+
return this.#autoSave
|
|
175
234
|
}
|
|
176
235
|
|
|
177
236
|
/**
|
|
@@ -187,22 +246,19 @@ export class TouchStorage<T extends object> {
|
|
|
187
246
|
*/
|
|
188
247
|
saveToRemote = useDebounceFn(async (options?: { force?: boolean }): Promise<void> => {
|
|
189
248
|
if (!channel) {
|
|
190
|
-
throw new Error(
|
|
249
|
+
throw new Error('TouchStorage: channel not initialized')
|
|
191
250
|
}
|
|
192
251
|
|
|
193
252
|
if (this.#assigning && !options?.force) {
|
|
194
|
-
|
|
195
|
-
return;
|
|
253
|
+
return
|
|
196
254
|
}
|
|
197
255
|
|
|
198
|
-
console.debug("Storage saveToRemote triggered", this.getQualifiedName());
|
|
199
|
-
|
|
200
256
|
await channel.send('storage:save', {
|
|
201
257
|
key: this.#qualifiedName,
|
|
202
258
|
content: JSON.stringify(this.data),
|
|
203
259
|
clear: false,
|
|
204
|
-
})
|
|
205
|
-
}, 300)
|
|
260
|
+
})
|
|
261
|
+
}, 300)
|
|
206
262
|
|
|
207
263
|
/**
|
|
208
264
|
* Enables or disables auto-saving.
|
|
@@ -216,34 +272,48 @@ export class TouchStorage<T extends object> {
|
|
|
216
272
|
* ```
|
|
217
273
|
*/
|
|
218
274
|
setAutoSave(autoSave: boolean): this {
|
|
219
|
-
this.#autoSave = autoSave
|
|
220
|
-
|
|
221
|
-
this.#autoSaveStopHandle?.()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
() => {
|
|
227
|
-
if (this.#assigning) {
|
|
228
|
-
console.debug("[Storage] Skip auto-save watch handle for", this.getQualifiedName());
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
this._onUpdate.forEach((fn) => {
|
|
233
|
-
try {
|
|
234
|
-
fn();
|
|
235
|
-
} catch (e) {
|
|
236
|
-
console.error(`[TouchStorage] onUpdate error in "${this.#qualifiedName}":`, e);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
this.saveToRemote();
|
|
241
|
-
},
|
|
242
|
-
{ deep: true, immediate: true },
|
|
243
|
-
);
|
|
275
|
+
this.#autoSave = autoSave
|
|
276
|
+
|
|
277
|
+
this.#autoSaveStopHandle?.()
|
|
278
|
+
this.#autoSaveStopHandle = undefined
|
|
279
|
+
|
|
280
|
+
if (autoSave && this.#channelInitialized) {
|
|
281
|
+
this.#startAutoSaveWatcher()
|
|
244
282
|
}
|
|
245
283
|
|
|
246
|
-
return this
|
|
284
|
+
return this
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#startAutoSaveWatcher(): void {
|
|
288
|
+
this.#autoSaveStopHandle = watch(
|
|
289
|
+
this.data,
|
|
290
|
+
() => {
|
|
291
|
+
if (this.#assigning) {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (this.#skipNextWatchTrigger) {
|
|
296
|
+
this.#skipNextWatchTrigger = false
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#runAutoSavePipeline()
|
|
301
|
+
},
|
|
302
|
+
{ deep: true, immediate: true },
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#runAutoSavePipeline(options?: { force?: boolean }): void {
|
|
307
|
+
this._onUpdate.forEach((fn) => {
|
|
308
|
+
try {
|
|
309
|
+
fn()
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
console.error(`[TouchStorage] onUpdate error in "${this.#qualifiedName}":`, e)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
this.saveToRemote(options)
|
|
247
317
|
}
|
|
248
318
|
|
|
249
319
|
/**
|
|
@@ -259,7 +329,7 @@ export class TouchStorage<T extends object> {
|
|
|
259
329
|
* ```
|
|
260
330
|
*/
|
|
261
331
|
onUpdate(fn: () => void): void {
|
|
262
|
-
this._onUpdate.push(fn)
|
|
332
|
+
this._onUpdate.push(fn)
|
|
263
333
|
}
|
|
264
334
|
|
|
265
335
|
/**
|
|
@@ -275,9 +345,9 @@ export class TouchStorage<T extends object> {
|
|
|
275
345
|
* ```
|
|
276
346
|
*/
|
|
277
347
|
offUpdate(fn: () => void): void {
|
|
278
|
-
const index = this._onUpdate.indexOf(fn)
|
|
348
|
+
const index = this._onUpdate.indexOf(fn)
|
|
279
349
|
if (index !== -1) {
|
|
280
|
-
this._onUpdate.splice(index, 1)
|
|
350
|
+
this._onUpdate.splice(index, 1)
|
|
281
351
|
}
|
|
282
352
|
}
|
|
283
353
|
|
|
@@ -297,18 +367,25 @@ export class TouchStorage<T extends object> {
|
|
|
297
367
|
*/
|
|
298
368
|
private assignData(newData: Partial<T>, stopWatch: boolean = true): void {
|
|
299
369
|
if (stopWatch && this.#autoSave) {
|
|
300
|
-
this.#assigning = true
|
|
301
|
-
console.debug(`[Storage] Stop auto-save watch handle for ${this.getQualifiedName()}`);
|
|
370
|
+
this.#assigning = true
|
|
302
371
|
}
|
|
303
372
|
|
|
304
|
-
Object.assign(this.data, newData)
|
|
305
|
-
console.debug(`[Storage] Assign data to ${this.getQualifiedName()}`);
|
|
373
|
+
Object.assign(this.data, newData)
|
|
306
374
|
|
|
307
375
|
if (stopWatch && this.#autoSave) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
376
|
+
this.#skipNextWatchTrigger = true
|
|
377
|
+
const resetAssigning = () => {
|
|
378
|
+
this.#assigning = false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (typeof queueMicrotask === 'function') {
|
|
382
|
+
queueMicrotask(resetAssigning)
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
Promise.resolve().then(resetAssigning)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.#runAutoSavePipeline({ force: true })
|
|
312
389
|
}
|
|
313
390
|
}
|
|
314
391
|
|
|
@@ -324,8 +401,8 @@ export class TouchStorage<T extends object> {
|
|
|
324
401
|
* ```
|
|
325
402
|
*/
|
|
326
403
|
applyData(data: Partial<T>): this {
|
|
327
|
-
this.assignData(data)
|
|
328
|
-
return this
|
|
404
|
+
this.assignData(data)
|
|
405
|
+
return this
|
|
329
406
|
}
|
|
330
407
|
|
|
331
408
|
/**
|
|
@@ -340,15 +417,16 @@ export class TouchStorage<T extends object> {
|
|
|
340
417
|
*/
|
|
341
418
|
async reloadFromRemote(): Promise<this> {
|
|
342
419
|
if (!channel) {
|
|
343
|
-
throw new Error(
|
|
420
|
+
throw new Error('TouchStorage: channel not initialized')
|
|
344
421
|
}
|
|
345
422
|
|
|
346
|
-
const result = await channel.send('storage:reload', this.#qualifiedName)
|
|
347
|
-
const parsed = result ? (result as Partial<T>) : {}
|
|
348
|
-
this.assignData(parsed, true)
|
|
423
|
+
const result = await channel.send('storage:reload', this.#qualifiedName)
|
|
424
|
+
const parsed = result ? (result as Partial<T>) : {}
|
|
425
|
+
this.assignData(parsed, true)
|
|
349
426
|
|
|
350
|
-
return this
|
|
427
|
+
return this
|
|
351
428
|
}
|
|
429
|
+
|
|
352
430
|
/**
|
|
353
431
|
* Loads data from remote storage and applies it.
|
|
354
432
|
* If channel is not initialized yet, this method will do nothing.
|
|
@@ -363,13 +441,43 @@ export class TouchStorage<T extends object> {
|
|
|
363
441
|
loadFromRemote(): this {
|
|
364
442
|
if (!channel) {
|
|
365
443
|
// Channel not initialized yet, data will be loaded when channel is ready
|
|
366
|
-
return this
|
|
444
|
+
return this
|
|
367
445
|
}
|
|
368
446
|
|
|
369
447
|
const result = channel.sendSync('storage:get', this.#qualifiedName)
|
|
370
|
-
const parsed = result ? (result as Partial<T>) : {}
|
|
371
|
-
this.assignData(parsed, true)
|
|
448
|
+
const parsed = result ? (result as Partial<T>) : {}
|
|
449
|
+
this.assignData(parsed, true)
|
|
372
450
|
|
|
373
|
-
return this
|
|
451
|
+
return this
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Gets the current data state.
|
|
456
|
+
*
|
|
457
|
+
* @returns Current data
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```ts
|
|
461
|
+
* const currentData = store.get();
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
get(): T {
|
|
465
|
+
return this.data as T
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Sets the entire data state.
|
|
470
|
+
*
|
|
471
|
+
* @param newData New data to replace current state
|
|
472
|
+
* @returns The current instance for chaining
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* store.set({ theme: 'dark', lang: 'en' });
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
set(newData: T): this {
|
|
480
|
+
this.assignData(newData as Partial<T>)
|
|
481
|
+
return this
|
|
374
482
|
}
|
|
375
483
|
}
|