@talex-touch/utils 1.0.13 → 1.0.15
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/base/index.ts +181 -181
- package/channel/index.ts +108 -99
- package/common/index.ts +2 -39
- package/common/storage/constants.ts +3 -0
- package/common/storage/entity/app-settings.ts +47 -0
- package/common/storage/entity/index.ts +1 -0
- package/common/storage/index.ts +3 -0
- package/common/utils.ts +160 -0
- package/core-box/README.md +218 -0
- package/core-box/index.ts +7 -0
- package/core-box/search.ts +536 -0
- package/core-box/types.ts +384 -0
- package/electron/download-manager.ts +118 -0
- package/{common → electron}/env-tool.ts +56 -56
- package/electron/touch-core.ts +167 -0
- package/electron/window.ts +71 -0
- package/eventbus/index.ts +86 -87
- package/index.ts +5 -0
- package/package.json +55 -30
- package/permission/index.ts +48 -48
- package/plugin/channel.ts +203 -193
- package/plugin/index.ts +216 -121
- package/plugin/log/logger-manager.ts +60 -0
- package/plugin/log/logger.ts +75 -0
- package/plugin/log/types.ts +27 -0
- package/plugin/preload.ts +39 -39
- package/plugin/sdk/common.ts +27 -27
- package/plugin/sdk/hooks/life-cycle.ts +95 -95
- package/plugin/sdk/index.ts +18 -13
- package/plugin/sdk/service/index.ts +29 -29
- package/plugin/sdk/types.ts +578 -0
- package/plugin/sdk/window/index.ts +40 -40
- package/renderer/index.ts +2 -0
- package/renderer/ref.ts +54 -54
- package/renderer/slots.ts +124 -0
- package/renderer/storage/app-settings.ts +34 -0
- package/renderer/storage/base-storage.ts +335 -0
- package/renderer/storage/index.ts +1 -0
- package/search/types.ts +726 -0
- package/service/index.ts +67 -67
- package/service/protocol/index.ts +77 -77
package/renderer/ref.ts
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
import { computed, customRef } from 'vue'
|
|
2
|
-
|
|
3
|
-
export function useModelWrapper(props: any, emit: any, name = 'modelValue') {
|
|
4
|
-
return computed({
|
|
5
|
-
get: () => props[name],
|
|
6
|
-
set: (value) => emit(`update:${name}`, value)
|
|
7
|
-
})
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function throttleRef(value: any, time: number) {
|
|
11
|
-
|
|
12
|
-
let ts = 0
|
|
13
|
-
|
|
14
|
-
return customRef((track, trigger) => {
|
|
15
|
-
return {
|
|
16
|
-
get() {
|
|
17
|
-
track()
|
|
18
|
-
return value
|
|
19
|
-
},
|
|
20
|
-
set(newValue) {
|
|
21
|
-
|
|
22
|
-
if( new Date().getTime() - ts < time ) return
|
|
23
|
-
|
|
24
|
-
value = newValue
|
|
25
|
-
track()
|
|
26
|
-
trigger()
|
|
27
|
-
ts = new Date().getTime()
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function debounceRef(value: any, delay: number) {
|
|
35
|
-
|
|
36
|
-
let timer: any
|
|
37
|
-
|
|
38
|
-
return customRef((track, trigger) => {
|
|
39
|
-
return {
|
|
40
|
-
get() {
|
|
41
|
-
track()
|
|
42
|
-
return value
|
|
43
|
-
},
|
|
44
|
-
set(newValue) {
|
|
45
|
-
clearTimeout(timer)
|
|
46
|
-
timer = setTimeout(() => {
|
|
47
|
-
value = newValue
|
|
48
|
-
track()
|
|
49
|
-
trigger()
|
|
50
|
-
}, delay)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
1
|
+
import { computed, customRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useModelWrapper(props: any, emit: any, name = 'modelValue') {
|
|
4
|
+
return computed({
|
|
5
|
+
get: () => props[name],
|
|
6
|
+
set: (value) => emit(`update:${name}`, value)
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function throttleRef(value: any, time: number) {
|
|
11
|
+
|
|
12
|
+
let ts = 0
|
|
13
|
+
|
|
14
|
+
return customRef((track, trigger) => {
|
|
15
|
+
return {
|
|
16
|
+
get() {
|
|
17
|
+
track()
|
|
18
|
+
return value
|
|
19
|
+
},
|
|
20
|
+
set(newValue) {
|
|
21
|
+
|
|
22
|
+
if( new Date().getTime() - ts < time ) return
|
|
23
|
+
|
|
24
|
+
value = newValue
|
|
25
|
+
track()
|
|
26
|
+
trigger()
|
|
27
|
+
ts = new Date().getTime()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function debounceRef(value: any, delay: number) {
|
|
35
|
+
|
|
36
|
+
let timer: any
|
|
37
|
+
|
|
38
|
+
return customRef((track, trigger) => {
|
|
39
|
+
return {
|
|
40
|
+
get() {
|
|
41
|
+
track()
|
|
42
|
+
return value
|
|
43
|
+
},
|
|
44
|
+
set(newValue) {
|
|
45
|
+
clearTimeout(timer)
|
|
46
|
+
timer = setTimeout(() => {
|
|
47
|
+
value = newValue
|
|
48
|
+
track()
|
|
49
|
+
trigger()
|
|
50
|
+
}, delay)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
55
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { VNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
type SlotSelector = string | string[] | ((name: string) => boolean);
|
|
4
|
+
type VNodePredicate = (node: VNode) => boolean;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes a slot input into a flat array of VNodes.
|
|
8
|
+
*
|
|
9
|
+
* @param input - Slot input, which can be a function, VNode, or array of VNodes.
|
|
10
|
+
* @returns A flat array of VNodes.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Normalize a mixed slot manually (VNode | VNode[] | () => VNode[])
|
|
15
|
+
* const vnodes = normalizeSlot(this.$slots.default);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeSlot(input: unknown): VNode[] {
|
|
19
|
+
if (!input) return [];
|
|
20
|
+
|
|
21
|
+
if (typeof input === 'function') {
|
|
22
|
+
return normalizeSlot(input());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(input)) {
|
|
26
|
+
return input.flatMap(normalizeSlot);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [input as VNode];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Recursively flattens a list of VNodes and filters them using an optional predicate.
|
|
34
|
+
*
|
|
35
|
+
* @param nodes - The VNodes to flatten and filter.
|
|
36
|
+
* @param predicate - Optional function to filter VNodes.
|
|
37
|
+
* @returns A flat array of VNodes that satisfy the predicate.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Flatten children of a given vnode array, filtering by custom type
|
|
42
|
+
* const flat = flattenVNodes(vnodes, vnode => typeof vnode.type === 'object');
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function flattenVNodes(
|
|
46
|
+
nodes: VNode[],
|
|
47
|
+
predicate?: VNodePredicate
|
|
48
|
+
): VNode[] {
|
|
49
|
+
const result: VNode[] = [];
|
|
50
|
+
|
|
51
|
+
for (const node of nodes) {
|
|
52
|
+
if (!node) continue;
|
|
53
|
+
|
|
54
|
+
if (predicate?.(node)) {
|
|
55
|
+
result.push(node);
|
|
56
|
+
} else if (node.children) {
|
|
57
|
+
const children = normalizeSlot(node.children);
|
|
58
|
+
result.push(...flattenVNodes(children, predicate));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extracts and flattens VNodes from one or more named slots,
|
|
67
|
+
* optionally filtered by a VNode predicate.
|
|
68
|
+
*
|
|
69
|
+
* @param slots - The slots object (e.g., this.$slots).
|
|
70
|
+
* @param slotSelector - Slot name(s) or a function to select slot names.
|
|
71
|
+
* @param predicate - Optional function to filter VNodes.
|
|
72
|
+
* @returns A flat array of matching VNodes from the selected slots.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* // 1. Extract from default slot and keep only nodes with name === 'MyComponent'
|
|
77
|
+
* const result1 = extractFromSlots(this.$slots, 'default', vnode => vnode.type?.name === 'MyComponent');
|
|
78
|
+
*
|
|
79
|
+
* // 2. Extract all VNodes from 'header' and 'footer' slots
|
|
80
|
+
* const result2 = extractFromSlots(this.$slots, ['header', 'footer']);
|
|
81
|
+
*
|
|
82
|
+
* // 3. Extract all VNodes from slots whose names start with 'section-'
|
|
83
|
+
* const result3 = extractFromSlots(this.$slots, name => name.startsWith('section-'));
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function extractFromSlots(
|
|
87
|
+
slots: Record<string, unknown>,
|
|
88
|
+
slotSelector: SlotSelector = 'default',
|
|
89
|
+
predicate?: VNodePredicate
|
|
90
|
+
): VNode[] {
|
|
91
|
+
const selectedSlotNames = resolveSlotNames(slots, slotSelector);
|
|
92
|
+
|
|
93
|
+
const vnodes = selectedSlotNames.flatMap((name) =>
|
|
94
|
+
normalizeSlot(slots[name])
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return flattenVNodes(vnodes, predicate);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolves slot names from the selector input.
|
|
102
|
+
*
|
|
103
|
+
* @param slots - The slots object.
|
|
104
|
+
* @param selector - A string, string array, or function.
|
|
105
|
+
* @returns An array of matched slot names.
|
|
106
|
+
*/
|
|
107
|
+
function resolveSlotNames(
|
|
108
|
+
slots: Record<string, unknown>,
|
|
109
|
+
selector: SlotSelector
|
|
110
|
+
): string[] {
|
|
111
|
+
if (typeof selector === 'string') {
|
|
112
|
+
return slots[selector] ? [selector] : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(selector)) {
|
|
116
|
+
return selector.filter((name) => !!slots[name]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof selector === 'function') {
|
|
120
|
+
return Object.keys(slots).filter(selector);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TouchStorage } from '.';
|
|
2
|
+
import { appSettingOriginData, StorageList, type AppSetting } from '../..';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Application settings storage manager
|
|
6
|
+
*
|
|
7
|
+
* Manages application configuration using `TouchStorage`, providing reactive data
|
|
8
|
+
* and automatic persistence.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { appSettings } from './app-settings-storage';
|
|
13
|
+
*
|
|
14
|
+
* // Read a setting
|
|
15
|
+
* const isAutoStart = appSettings.data.autoStart;
|
|
16
|
+
*
|
|
17
|
+
* // Modify a setting (auto-saved)
|
|
18
|
+
* appSettings.data.autoStart = true;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
class AppSettingsStorage extends TouchStorage<AppSetting> {
|
|
22
|
+
/**
|
|
23
|
+
* Initializes a new instance of the AppSettingsStorage class
|
|
24
|
+
*/
|
|
25
|
+
constructor() {
|
|
26
|
+
super(StorageList.APP_SETTING, JSON.parse(JSON.stringify(appSettingOriginData)));
|
|
27
|
+
this.setAutoSave(true);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Global instance of the application settings
|
|
33
|
+
*/
|
|
34
|
+
export const appSettings = new AppSettingsStorage();
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reactive,
|
|
3
|
+
watch,
|
|
4
|
+
type UnwrapNestedRefs,
|
|
5
|
+
type WatchHandle,
|
|
6
|
+
} from 'vue';
|
|
7
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
8
|
+
import type { ITouchClientChannel } from '../../channel';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Interface representing the external communication channel.
|
|
12
|
+
* Must be initialized before any `TouchStorage` instance is used.
|
|
13
|
+
*/
|
|
14
|
+
export interface IStorageChannel extends ITouchClientChannel {
|
|
15
|
+
/**
|
|
16
|
+
* Asynchronous send interface
|
|
17
|
+
* @param event Event name
|
|
18
|
+
* @param payload Event payload
|
|
19
|
+
*/
|
|
20
|
+
send(event: string, payload: unknown): Promise<unknown>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Synchronous send interface
|
|
24
|
+
* @param event Event name
|
|
25
|
+
* @param payload Event payload
|
|
26
|
+
*/
|
|
27
|
+
sendSync(event: string, payload: unknown): unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let channel: IStorageChannel | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initializes the global channel for communication.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { initStorageChannel } from './TouchStorage';
|
|
38
|
+
* import { ipcRenderer } from 'electron';
|
|
39
|
+
*
|
|
40
|
+
* initStorageChannel({
|
|
41
|
+
* send: ipcRenderer.invoke.bind(ipcRenderer),
|
|
42
|
+
* sendSync: ipcRenderer.sendSync.bind(ipcRenderer),
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function initStorageChannel(c: IStorageChannel): void {
|
|
47
|
+
channel = c;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Global registry of storage instances.
|
|
52
|
+
*/
|
|
53
|
+
export const storages = new Map<string, TouchStorage<any>>();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A reactive storage utility with optional auto-save and update subscriptions.
|
|
57
|
+
*
|
|
58
|
+
* @template T Shape of the stored data.
|
|
59
|
+
*/
|
|
60
|
+
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> = [];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The reactive data exposed to users.
|
|
70
|
+
*/
|
|
71
|
+
public data: UnwrapNestedRefs<T>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a new reactive storage instance.
|
|
75
|
+
*
|
|
76
|
+
* @param qName Globally unique name for the instance
|
|
77
|
+
* @param initData Initial data to populate the storage
|
|
78
|
+
* @param onUpdate Optional callback when data is updated
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const settings = new TouchStorage('settings', { darkMode: false });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
constructor(qName: string, initData: T, onUpdate?: () => void) {
|
|
86
|
+
if (storages.has(qName)) {
|
|
87
|
+
throw new Error(`Storage "${qName}" already exists`);
|
|
88
|
+
}
|
|
89
|
+
if (!channel) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'TouchStorage: channel is not initialized. Please call initStorageChannel(...) before using.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.#qualifiedName = qName;
|
|
96
|
+
this.originalData = initData;
|
|
97
|
+
|
|
98
|
+
// const stored = (channel.sendSync('storage:get', qName) as Partial<T>) || {};
|
|
99
|
+
this.data = reactive({ ...initData }) as UnwrapNestedRefs<T>;
|
|
100
|
+
this.loadFromRemote()
|
|
101
|
+
|
|
102
|
+
if (onUpdate) this._onUpdate.push(onUpdate);
|
|
103
|
+
|
|
104
|
+
channel.regChannel('storage:update', ({ data }) => {
|
|
105
|
+
const { name } = data!
|
|
106
|
+
|
|
107
|
+
if (name === qName) {
|
|
108
|
+
this.loadFromRemote()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
storages.set(qName, this);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the unique identifier of this storage.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* console.log(userStore.getQualifiedName()); // "user"
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
getQualifiedName(): string {
|
|
124
|
+
return this.#qualifiedName;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks whether auto-save is currently enabled.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* if (store.isAutoSave()) console.log("Auto-save is on!");
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
isAutoSave(): boolean {
|
|
136
|
+
return this.#autoSave;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Saves the current data to remote storage.
|
|
141
|
+
*
|
|
142
|
+
* @param options Optional configuration
|
|
143
|
+
* @param options.force Force save even if data is being assigned
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* await store.saveToRemote();
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
saveToRemote = useDebounceFn(async (options?: { force?: boolean }): Promise<void> => {
|
|
151
|
+
if (!channel) {
|
|
152
|
+
throw new Error("TouchStorage: channel not initialized");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.#assigning && !options?.force) {
|
|
156
|
+
console.debug("[Storage] Skip saveToRemote for", this.getQualifiedName());
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.debug("Storage saveToRemote triggered", this.getQualifiedName());
|
|
161
|
+
|
|
162
|
+
await channel.send('storage:save', {
|
|
163
|
+
key: this.#qualifiedName,
|
|
164
|
+
content: JSON.stringify(this.data),
|
|
165
|
+
clear: false,
|
|
166
|
+
});
|
|
167
|
+
}, 300);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Enables or disables auto-saving.
|
|
171
|
+
*
|
|
172
|
+
* @param autoSave Whether to enable auto-saving
|
|
173
|
+
* @returns The current instance for chaining
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* store.setAutoSave(true);
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
setAutoSave(autoSave: boolean): this {
|
|
181
|
+
this.#autoSave = autoSave;
|
|
182
|
+
|
|
183
|
+
this.#autoSaveStopHandle?.();
|
|
184
|
+
|
|
185
|
+
if (autoSave) {
|
|
186
|
+
this.#autoSaveStopHandle = watch(
|
|
187
|
+
this.data,
|
|
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
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Registers a callback that runs when data changes (only triggered in auto-save mode).
|
|
213
|
+
*
|
|
214
|
+
* @param fn Callback function
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* store.onUpdate(() => {
|
|
219
|
+
* console.log('Data changed');
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
onUpdate(fn: () => void): void {
|
|
224
|
+
this._onUpdate.push(fn);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Removes a previously registered update callback.
|
|
229
|
+
*
|
|
230
|
+
* @param fn The same callback used in `onUpdate`
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* const cb = () => console.log("Change!");
|
|
235
|
+
* store.onUpdate(cb);
|
|
236
|
+
* store.offUpdate(cb);
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
offUpdate(fn: () => void): void {
|
|
240
|
+
const index = this._onUpdate.indexOf(fn);
|
|
241
|
+
if (index !== -1) {
|
|
242
|
+
this._onUpdate.splice(index, 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Internal method to assign new values and trigger update events. (Debounced)
|
|
248
|
+
*
|
|
249
|
+
* @param newData Partial update data
|
|
250
|
+
* @param stopWatch Whether to stop the watcher after assignment
|
|
251
|
+
*/
|
|
252
|
+
assignDataDebounced = useDebounceFn(this.assignData.bind(this), 100)
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Internal method to assign new values and trigger update events.
|
|
256
|
+
*
|
|
257
|
+
* @param newData Partial update data
|
|
258
|
+
* @param stopWatch Whether to stop the watcher during assignment
|
|
259
|
+
*/
|
|
260
|
+
private assignData(newData: Partial<T>, stopWatch: boolean = true): void {
|
|
261
|
+
if (stopWatch && this.#autoSave) {
|
|
262
|
+
this.#assigning = true;
|
|
263
|
+
console.debug(`[Storage] Stop auto-save watch handle for ${this.getQualifiedName()}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Object.assign(this.data, newData);
|
|
267
|
+
console.debug(`[Storage] Assign data to ${this.getQualifiedName()}`);
|
|
268
|
+
|
|
269
|
+
if (stopWatch && this.#autoSave) {
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
this.#assigning = false;
|
|
272
|
+
console.debug(`[Storage] Resume auto-save watch handle for ${this.getQualifiedName()}`);
|
|
273
|
+
}, 0);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Applies new data to the current storage instance. Use with caution.
|
|
279
|
+
*
|
|
280
|
+
* @param data Partial object to merge into current data
|
|
281
|
+
* @returns The current instance for chaining
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* store.applyData({ theme: 'dark' });
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
applyData(data: Partial<T>): this {
|
|
289
|
+
this.assignData(data);
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Reloads data from remote storage and applies it.
|
|
295
|
+
*
|
|
296
|
+
* @returns The current instance
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* await store.reloadFromRemote();
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
async reloadFromRemote(): Promise<this> {
|
|
304
|
+
if (!channel) {
|
|
305
|
+
throw new Error("TouchStorage: channel not initialized");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const result = await channel.send('storage:reload', this.#qualifiedName);
|
|
309
|
+
const parsed = result ? (result as Partial<T>) : {};
|
|
310
|
+
this.assignData(parsed, true);
|
|
311
|
+
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Loads data from remote storage and applies it.
|
|
316
|
+
*
|
|
317
|
+
* @returns The current instance
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* store.loadFromRemote();
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
loadFromRemote(): this {
|
|
325
|
+
if (!channel) {
|
|
326
|
+
throw new Error("TouchStorage: channel not initialized");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = channel.sendSync('storage:get', this.#qualifiedName)
|
|
330
|
+
const parsed = result ? (result as Partial<T>) : {};
|
|
331
|
+
this.assignData(parsed, true);
|
|
332
|
+
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './base-storage'
|