@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.
Files changed (116) hide show
  1. package/animation/window-node.ts +15 -12
  2. package/animation/window.ts +19 -15
  3. package/auth/clerk-types.ts +1 -1
  4. package/auth/index.ts +1 -1
  5. package/auth/useAuthState.ts +6 -5
  6. package/auth/useClerkConfig.ts +4 -4
  7. package/auth/useClerkProvider.ts +3 -2
  8. package/channel/index.ts +23 -22
  9. package/common/file-scan-constants.ts +137 -121
  10. package/common/file-scan-utils.ts +48 -27
  11. package/common/index.ts +3 -3
  12. package/common/search/gather.ts +1 -1
  13. package/common/search/index.ts +5 -6
  14. package/common/storage/constants.ts +3 -2
  15. package/common/storage/entity/app-settings.ts +5 -3
  16. package/common/storage/entity/shortcut-settings.ts +10 -10
  17. package/common/storage/shortcut-storage.ts +6 -4
  18. package/common/utils/file.ts +14 -6
  19. package/common/utils/index.ts +62 -52
  20. package/common/utils/polling.ts +88 -84
  21. package/common/utils/task-queue.ts +11 -10
  22. package/common/utils/time.ts +50 -47
  23. package/common/utils/timing.ts +41 -37
  24. package/core-box/builder/index.ts +1 -1
  25. package/core-box/builder/tuff-builder.ts +254 -229
  26. package/core-box/index.ts +4 -6
  27. package/core-box/preview/index.ts +1 -0
  28. package/core-box/preview/types.ts +43 -0
  29. package/core-box/recommendation.ts +77 -0
  30. package/core-box/tuff/index.ts +1 -1
  31. package/core-box/tuff/tuff-dsl.ts +328 -266
  32. package/electron/download-manager.ts +43 -42
  33. package/electron/env-tool.ts +19 -18
  34. package/electron/file-parsers/index.ts +2 -2
  35. package/electron/file-parsers/parsers/text-parser.ts +15 -14
  36. package/electron/file-parsers/registry.ts +9 -7
  37. package/electron/file-parsers/types.ts +4 -4
  38. package/electron/index.ts +2 -2
  39. package/eventbus/index.ts +11 -11
  40. package/index.ts +5 -4
  41. package/intelligence/client.ts +87 -0
  42. package/intelligence/index.ts +1 -0
  43. package/package.json +14 -14
  44. package/permission/index.ts +8 -8
  45. package/plugin/channel.ts +77 -68
  46. package/plugin/index.ts +96 -82
  47. package/plugin/install.ts +8 -8
  48. package/plugin/log/types.ts +5 -5
  49. package/plugin/node/index.ts +1 -1
  50. package/plugin/node/logger-manager.ts +14 -11
  51. package/plugin/node/logger.ts +8 -8
  52. package/plugin/plugin-source.ts +11 -11
  53. package/plugin/preload.ts +1 -1
  54. package/plugin/providers/registry.ts +8 -7
  55. package/plugin/providers/types.ts +6 -6
  56. package/plugin/sdk/README.md +216 -0
  57. package/plugin/sdk/box-sdk.ts +219 -0
  58. package/plugin/sdk/channel.ts +20 -20
  59. package/plugin/sdk/clipboard.ts +8 -6
  60. package/plugin/sdk/common.ts +10 -6
  61. package/plugin/sdk/core-box.ts +2 -3
  62. package/plugin/sdk/division-box.ts +266 -0
  63. package/plugin/sdk/enum/bridge-event.ts +1 -1
  64. package/plugin/sdk/examples/storage-onDidChange-example.js +1 -1
  65. package/plugin/sdk/feature-sdk.ts +235 -0
  66. package/plugin/sdk/features.ts +34 -26
  67. package/plugin/sdk/hooks/bridge.ts +3 -6
  68. package/plugin/sdk/hooks/index.ts +1 -1
  69. package/plugin/sdk/hooks/life-cycle.ts +4 -10
  70. package/plugin/sdk/index.ts +10 -7
  71. package/plugin/sdk/service/index.ts +3 -3
  72. package/plugin/sdk/storage.ts +4 -4
  73. package/plugin/sdk/system.ts +1 -1
  74. package/plugin/sdk/types.ts +165 -146
  75. package/plugin/sdk/window/index.ts +8 -5
  76. package/preload/loading.ts +6 -6
  77. package/preload/renderer.ts +4 -2
  78. package/renderer/hooks/arg-mapper.ts +1 -2
  79. package/renderer/hooks/index.ts +2 -0
  80. package/renderer/hooks/initialize.ts +10 -8
  81. package/renderer/hooks/performance.ts +4 -4
  82. package/renderer/hooks/use-channel.ts +150 -0
  83. package/renderer/hooks/use-intelligence.ts +236 -0
  84. package/renderer/index.ts +6 -2
  85. package/renderer/ref.ts +32 -36
  86. package/renderer/slots.ts +29 -26
  87. package/renderer/storage/app-settings.ts +16 -6
  88. package/renderer/storage/base-storage.ts +222 -114
  89. package/renderer/storage/index.ts +3 -0
  90. package/renderer/storage/intelligence-storage.ts +218 -0
  91. package/renderer/storage/openers.ts +13 -3
  92. package/renderer/touch-sdk/env.ts +41 -41
  93. package/renderer/touch-sdk/index.ts +1 -1
  94. package/renderer/touch-sdk/terminal.ts +5 -5
  95. package/renderer/touch-sdk/utils.ts +4 -3
  96. package/search/levenshtein-utils.ts +11 -11
  97. package/search/types.ts +102 -102
  98. package/service/index.ts +11 -11
  99. package/service/protocol/index.ts +217 -14
  100. package/types/division-box.ts +248 -0
  101. package/types/download.ts +72 -34
  102. package/types/index.ts +3 -1
  103. package/types/intelligence.ts +607 -0
  104. package/types/modules/base.ts +16 -16
  105. package/types/modules/index.ts +1 -1
  106. package/types/modules/module-lifecycle.ts +21 -21
  107. package/types/modules/module-manager.ts +11 -11
  108. package/types/modules/module.ts +16 -16
  109. package/types/storage.ts +0 -1
  110. package/types/touch-app-core.ts +32 -32
  111. package/types/update.ts +91 -21
  112. package/core-box/README.md +0 -218
  113. package/core-box/builder/tuff-builder.example.ts.bak +0 -258
  114. package/core-box/run-tests.sh +0 -7
  115. package/core-box/search.ts +0 -1
  116. 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) return [];
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) continue;
53
+ if (!node)
54
+ continue
53
55
 
54
56
  if (predicate?.(node)) {
55
- result.push(node);
56
- } else if (node.children) {
57
- const children = normalizeSlot(node.children);
58
- result.push(...flattenVNodes(children, predicate));
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((name) =>
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((name) => !!slots[name]);
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 { TouchStorage } from '.';
2
- import { appSettingOriginData, StorageList, type AppSetting } from '../..';
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
- * // Read a setting
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
- export const appSettings = new AppSettingsStorage();
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
- type UnwrapNestedRefs,
5
- type WatchHandle,
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): Promise<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): 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
- * Processes all pending storage initializations after initialization.
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
- // Process all pending storage initializations
56
- for (const initFn of pendingInitializations) {
57
- initFn();
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
- // Clear the queue
61
- pendingInitializations.length = 0;
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
- * Global registry of storage instances.
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 const storages = new Map<string, TouchStorage<any>>();
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
- * If channel is not initialized, the instance will be queued for initialization.
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) this._onUpdate.push(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
- if (channel) {
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
- if (!channel) {
133
- throw new Error(
134
- 'TouchStorage: channel is not initialized. Please call initStorageChannel(...) before using.'
135
- );
136
- }
190
+ this.#channelInitialized = true
137
191
 
138
- this.#channelInitialized = true;
192
+ const result = channel!.sendSync('storage:get', this.#qualifiedName)
193
+ const parsed = result ? (result as Partial<T>) : {}
139
194
 
140
- // Load data from remote
141
- this.loadFromRemote();
195
+ this.assignData(parsed)
142
196
 
143
197
  // Register update listener
144
- channel.regChannel('storage:update', ({ data }) => {
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("TouchStorage: channel not initialized");
249
+ throw new Error('TouchStorage: channel not initialized')
191
250
  }
192
251
 
193
252
  if (this.#assigning && !options?.force) {
194
- console.debug("[Storage] Skip saveToRemote for", this.getQualifiedName());
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
- if (autoSave) {
224
- this.#autoSaveStopHandle = watch(
225
- this.data,
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
- setTimeout(() => {
309
- this.#assigning = false;
310
- console.debug(`[Storage] Resume auto-save watch handle for ${this.getQualifiedName()}`);
311
- }, 0);
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("TouchStorage: channel not initialized");
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
  }
@@ -1 +1,4 @@
1
+ export * from './app-settings'
1
2
  export * from './base-storage'
3
+ export * from './intelligence-storage'
4
+ export * from './openers'