@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.
- package/animation/window-node.ts +205 -0
- 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 +6 -6
- package/auth/useClerkProvider.ts +3 -2
- package/channel/index.ts +28 -21
- package/common/file-scan-constants.ts +137 -121
- package/common/file-scan-utils.ts +49 -25
- 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 +19 -3
- package/common/storage/entity/shortcut-settings.ts +10 -10
- package/common/storage/shortcut-storage.ts +6 -4
- package/common/utils/file.ts +15 -4
- package/common/utils/index.ts +62 -52
- package/common/utils/polling.ts +114 -63
- 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 +255 -230
- package/core-box/index.ts +3 -6
- package/core-box/preview/index.ts +1 -0
- package/core-box/preview/types.ts +43 -0
- package/core-box/tuff/index.ts +1 -1
- package/core-box/tuff/tuff-dsl.ts +419 -253
- package/electron/clipboard-helper.ts +20 -12
- 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 +1 -1
- package/eventbus/index.ts +11 -11
- package/index.ts +6 -5
- 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 +113 -84
- 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 +6 -3
- package/plugin/providers/registry.ts +8 -7
- package/plugin/providers/types.ts +6 -6
- 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/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 +9 -13
- 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 +169 -143
- 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 -1
- 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 +236 -88
- package/renderer/storage/index.ts +3 -0
- package/renderer/storage/intelligence-storage.ts +215 -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 -103
- 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/icon.ts +2 -1
- package/types/index.ts +3 -1
- package/types/intelligence.ts +413 -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 +79 -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/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,20 +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
|
+
let channel: IStorageChannel | null = null
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Initializes the global channel for communication.
|
|
35
|
+
* Must be called before creating any TouchStorage instances.
|
|
34
36
|
*
|
|
35
37
|
* @example
|
|
36
38
|
* ```ts
|
|
@@ -44,13 +46,72 @@ let channel: IStorageChannel | null = null;
|
|
|
44
46
|
* ```
|
|
45
47
|
*/
|
|
46
48
|
export function initStorageChannel(c: IStorageChannel): void {
|
|
47
|
-
channel = c
|
|
49
|
+
channel = c
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Global registry of storage instances.
|
|
52
54
|
*/
|
|
53
|
-
|
|
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
|
|
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
|
+
}
|
|
68
|
+
|
|
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]!
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
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.
|
|
88
|
+
*/
|
|
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
|
+
}
|
|
54
115
|
|
|
55
116
|
/**
|
|
56
117
|
* A reactive storage utility with optional auto-save and update subscriptions.
|
|
@@ -58,58 +119,94 @@ export const storages = new Map<string, TouchStorage<any>>();
|
|
|
58
119
|
* @template T Shape of the stored data.
|
|
59
120
|
*/
|
|
60
121
|
export class TouchStorage<T extends object> {
|
|
61
|
-
readonly #qualifiedName: string
|
|
62
|
-
#autoSave = false
|
|
63
|
-
#autoSaveStopHandle?: WatchHandle
|
|
64
|
-
#assigning = false
|
|
65
|
-
readonly originalData: T
|
|
66
|
-
private readonly _onUpdate: Array<() => void> = []
|
|
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
|
|
67
130
|
|
|
68
131
|
/**
|
|
69
132
|
* The reactive data exposed to users.
|
|
70
133
|
*/
|
|
71
|
-
public data: UnwrapNestedRefs<T
|
|
134
|
+
public data: UnwrapNestedRefs<T>
|
|
72
135
|
|
|
73
136
|
/**
|
|
74
137
|
* Creates a new reactive storage instance.
|
|
138
|
+
* IMPORTANT: `initStorageChannel()` must be called before creating any TouchStorage instances.
|
|
75
139
|
*
|
|
76
140
|
* @param qName Globally unique name for the instance
|
|
77
141
|
* @param initData Initial data to populate the storage
|
|
78
142
|
* @param onUpdate Optional callback when data is updated
|
|
79
143
|
*
|
|
144
|
+
* @throws {Error} If channel is not initialized or if storage with same name already exists
|
|
145
|
+
*
|
|
80
146
|
* @example
|
|
81
147
|
* ```ts
|
|
148
|
+
* // First initialize the channel
|
|
149
|
+
* initStorageChannel(touchChannel);
|
|
150
|
+
*
|
|
151
|
+
* // Then create storage instances
|
|
82
152
|
* const settings = new TouchStorage('settings', { darkMode: false });
|
|
83
153
|
* ```
|
|
84
154
|
*/
|
|
85
155
|
constructor(qName: string, initData: T, onUpdate?: () => void) {
|
|
86
|
-
if (storages.has(qName)) {
|
|
87
|
-
throw new Error(`Storage "${qName}" already exists`);
|
|
88
|
-
}
|
|
89
156
|
if (!channel) {
|
|
90
157
|
throw new Error(
|
|
91
|
-
|
|
92
|
-
|
|
158
|
+
`TouchStorage: Cannot create storage "${qName}" before channel is initialized. `
|
|
159
|
+
+ 'Please call initStorageChannel() first.',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (storages.has(qName)) {
|
|
164
|
+
throw new Error(`Storage "${qName}" already exists`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.#qualifiedName = qName
|
|
168
|
+
this.originalData = initData
|
|
169
|
+
this.data = reactive({ ...initData }) as UnwrapNestedRefs<T>
|
|
170
|
+
|
|
171
|
+
if (onUpdate)
|
|
172
|
+
this._onUpdate.push(onUpdate)
|
|
173
|
+
|
|
174
|
+
// Register to storages map immediately
|
|
175
|
+
storages.set(qName, this)
|
|
176
|
+
|
|
177
|
+
// Initialize channel-dependent operations immediately
|
|
178
|
+
this.#initializeChannel()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Initialize channel-dependent operations.
|
|
183
|
+
* Called immediately in constructor after channel validation.
|
|
184
|
+
*/
|
|
185
|
+
#initializeChannel(): void {
|
|
186
|
+
if (this.#channelInitialized) {
|
|
187
|
+
return
|
|
93
188
|
}
|
|
94
189
|
|
|
95
|
-
this.#
|
|
96
|
-
this.originalData = initData;
|
|
190
|
+
this.#channelInitialized = true
|
|
97
191
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.loadFromRemote()
|
|
192
|
+
const result = channel!.sendSync('storage:get', this.#qualifiedName)
|
|
193
|
+
const parsed = result ? (result as Partial<T>) : {}
|
|
101
194
|
|
|
102
|
-
|
|
195
|
+
this.assignData(parsed)
|
|
103
196
|
|
|
104
|
-
|
|
197
|
+
// Register update listener
|
|
198
|
+
channel!.regChannel('storage:update', ({ data }) => {
|
|
105
199
|
const { name } = data!
|
|
106
200
|
|
|
107
|
-
if (name ===
|
|
201
|
+
if (name === this.#qualifiedName) {
|
|
108
202
|
this.loadFromRemote()
|
|
109
203
|
}
|
|
110
204
|
})
|
|
111
205
|
|
|
112
|
-
|
|
206
|
+
// Start auto-save watcher AFTER initial data load
|
|
207
|
+
if (this.#autoSave && !this.#autoSaveStopHandle) {
|
|
208
|
+
this.#startAutoSaveWatcher()
|
|
209
|
+
}
|
|
113
210
|
}
|
|
114
211
|
|
|
115
212
|
/**
|
|
@@ -121,7 +218,7 @@ export class TouchStorage<T extends object> {
|
|
|
121
218
|
* ```
|
|
122
219
|
*/
|
|
123
220
|
getQualifiedName(): string {
|
|
124
|
-
return this.#qualifiedName
|
|
221
|
+
return this.#qualifiedName
|
|
125
222
|
}
|
|
126
223
|
|
|
127
224
|
/**
|
|
@@ -133,7 +230,7 @@ export class TouchStorage<T extends object> {
|
|
|
133
230
|
* ```
|
|
134
231
|
*/
|
|
135
232
|
isAutoSave(): boolean {
|
|
136
|
-
return this.#autoSave
|
|
233
|
+
return this.#autoSave
|
|
137
234
|
}
|
|
138
235
|
|
|
139
236
|
/**
|
|
@@ -149,22 +246,19 @@ export class TouchStorage<T extends object> {
|
|
|
149
246
|
*/
|
|
150
247
|
saveToRemote = useDebounceFn(async (options?: { force?: boolean }): Promise<void> => {
|
|
151
248
|
if (!channel) {
|
|
152
|
-
throw new Error(
|
|
249
|
+
throw new Error('TouchStorage: channel not initialized')
|
|
153
250
|
}
|
|
154
251
|
|
|
155
252
|
if (this.#assigning && !options?.force) {
|
|
156
|
-
|
|
157
|
-
return;
|
|
253
|
+
return
|
|
158
254
|
}
|
|
159
255
|
|
|
160
|
-
console.debug("Storage saveToRemote triggered", this.getQualifiedName());
|
|
161
|
-
|
|
162
256
|
await channel.send('storage:save', {
|
|
163
257
|
key: this.#qualifiedName,
|
|
164
258
|
content: JSON.stringify(this.data),
|
|
165
259
|
clear: false,
|
|
166
|
-
})
|
|
167
|
-
}, 300)
|
|
260
|
+
})
|
|
261
|
+
}, 300)
|
|
168
262
|
|
|
169
263
|
/**
|
|
170
264
|
* Enables or disables auto-saving.
|
|
@@ -178,34 +272,48 @@ export class TouchStorage<T extends object> {
|
|
|
178
272
|
* ```
|
|
179
273
|
*/
|
|
180
274
|
setAutoSave(autoSave: boolean): this {
|
|
181
|
-
this.#autoSave = autoSave
|
|
182
|
-
|
|
183
|
-
this.#autoSaveStopHandle?.()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
() => {
|
|
189
|
-
if (this.#assigning) {
|
|
190
|
-
console.debug("[Storage] Skip auto-save watch handle for", this.getQualifiedName());
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this._onUpdate.forEach((fn) => {
|
|
195
|
-
try {
|
|
196
|
-
fn();
|
|
197
|
-
} catch (e) {
|
|
198
|
-
console.error(`[TouchStorage] onUpdate error in "${this.#qualifiedName}":`, e);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
this.saveToRemote();
|
|
203
|
-
},
|
|
204
|
-
{ deep: true, immediate: true },
|
|
205
|
-
);
|
|
275
|
+
this.#autoSave = autoSave
|
|
276
|
+
|
|
277
|
+
this.#autoSaveStopHandle?.()
|
|
278
|
+
this.#autoSaveStopHandle = undefined
|
|
279
|
+
|
|
280
|
+
if (autoSave && this.#channelInitialized) {
|
|
281
|
+
this.#startAutoSaveWatcher()
|
|
206
282
|
}
|
|
207
283
|
|
|
208
|
-
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)
|
|
209
317
|
}
|
|
210
318
|
|
|
211
319
|
/**
|
|
@@ -221,7 +329,7 @@ export class TouchStorage<T extends object> {
|
|
|
221
329
|
* ```
|
|
222
330
|
*/
|
|
223
331
|
onUpdate(fn: () => void): void {
|
|
224
|
-
this._onUpdate.push(fn)
|
|
332
|
+
this._onUpdate.push(fn)
|
|
225
333
|
}
|
|
226
334
|
|
|
227
335
|
/**
|
|
@@ -237,9 +345,9 @@ export class TouchStorage<T extends object> {
|
|
|
237
345
|
* ```
|
|
238
346
|
*/
|
|
239
347
|
offUpdate(fn: () => void): void {
|
|
240
|
-
const index = this._onUpdate.indexOf(fn)
|
|
348
|
+
const index = this._onUpdate.indexOf(fn)
|
|
241
349
|
if (index !== -1) {
|
|
242
|
-
this._onUpdate.splice(index, 1)
|
|
350
|
+
this._onUpdate.splice(index, 1)
|
|
243
351
|
}
|
|
244
352
|
}
|
|
245
353
|
|
|
@@ -259,18 +367,25 @@ export class TouchStorage<T extends object> {
|
|
|
259
367
|
*/
|
|
260
368
|
private assignData(newData: Partial<T>, stopWatch: boolean = true): void {
|
|
261
369
|
if (stopWatch && this.#autoSave) {
|
|
262
|
-
this.#assigning = true
|
|
263
|
-
console.debug(`[Storage] Stop auto-save watch handle for ${this.getQualifiedName()}`);
|
|
370
|
+
this.#assigning = true
|
|
264
371
|
}
|
|
265
372
|
|
|
266
|
-
Object.assign(this.data, newData)
|
|
267
|
-
console.debug(`[Storage] Assign data to ${this.getQualifiedName()}`);
|
|
373
|
+
Object.assign(this.data, newData)
|
|
268
374
|
|
|
269
375
|
if (stopWatch && this.#autoSave) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
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 })
|
|
274
389
|
}
|
|
275
390
|
}
|
|
276
391
|
|
|
@@ -286,8 +401,8 @@ export class TouchStorage<T extends object> {
|
|
|
286
401
|
* ```
|
|
287
402
|
*/
|
|
288
403
|
applyData(data: Partial<T>): this {
|
|
289
|
-
this.assignData(data)
|
|
290
|
-
return this
|
|
404
|
+
this.assignData(data)
|
|
405
|
+
return this
|
|
291
406
|
}
|
|
292
407
|
|
|
293
408
|
/**
|
|
@@ -302,17 +417,19 @@ export class TouchStorage<T extends object> {
|
|
|
302
417
|
*/
|
|
303
418
|
async reloadFromRemote(): Promise<this> {
|
|
304
419
|
if (!channel) {
|
|
305
|
-
throw new Error(
|
|
420
|
+
throw new Error('TouchStorage: channel not initialized')
|
|
306
421
|
}
|
|
307
422
|
|
|
308
|
-
const result = await channel.send('storage:reload', this.#qualifiedName)
|
|
309
|
-
const parsed = result ? (result as Partial<T>) : {}
|
|
310
|
-
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)
|
|
311
426
|
|
|
312
|
-
return this
|
|
427
|
+
return this
|
|
313
428
|
}
|
|
429
|
+
|
|
314
430
|
/**
|
|
315
431
|
* Loads data from remote storage and applies it.
|
|
432
|
+
* If channel is not initialized yet, this method will do nothing.
|
|
316
433
|
*
|
|
317
434
|
* @returns The current instance
|
|
318
435
|
*
|
|
@@ -323,13 +440,44 @@ export class TouchStorage<T extends object> {
|
|
|
323
440
|
*/
|
|
324
441
|
loadFromRemote(): this {
|
|
325
442
|
if (!channel) {
|
|
326
|
-
|
|
443
|
+
// Channel not initialized yet, data will be loaded when channel is ready
|
|
444
|
+
return this
|
|
327
445
|
}
|
|
328
446
|
|
|
329
447
|
const result = channel.sendSync('storage:get', this.#qualifiedName)
|
|
330
|
-
const parsed = result ? (result as Partial<T>) : {}
|
|
331
|
-
this.assignData(parsed, true)
|
|
448
|
+
const parsed = result ? (result as Partial<T>) : {}
|
|
449
|
+
this.assignData(parsed, true)
|
|
332
450
|
|
|
333
|
-
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
|
|
334
482
|
}
|
|
335
483
|
}
|