@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.
Files changed (113) hide show
  1. package/animation/window-node.ts +205 -0
  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 +6 -6
  7. package/auth/useClerkProvider.ts +3 -2
  8. package/channel/index.ts +28 -21
  9. package/common/file-scan-constants.ts +137 -121
  10. package/common/file-scan-utils.ts +49 -25
  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 +19 -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 +15 -4
  19. package/common/utils/index.ts +62 -52
  20. package/common/utils/polling.ts +114 -63
  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 +255 -230
  26. package/core-box/index.ts +3 -6
  27. package/core-box/preview/index.ts +1 -0
  28. package/core-box/preview/types.ts +43 -0
  29. package/core-box/tuff/index.ts +1 -1
  30. package/core-box/tuff/tuff-dsl.ts +419 -253
  31. package/electron/clipboard-helper.ts +20 -12
  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 +1 -1
  39. package/eventbus/index.ts +11 -11
  40. package/index.ts +6 -5
  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 +113 -84
  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 +6 -3
  54. package/plugin/providers/registry.ts +8 -7
  55. package/plugin/providers/types.ts +6 -6
  56. package/plugin/sdk/channel.ts +20 -20
  57. package/plugin/sdk/clipboard.ts +8 -6
  58. package/plugin/sdk/common.ts +10 -6
  59. package/plugin/sdk/core-box.ts +2 -3
  60. package/plugin/sdk/division-box.ts +266 -0
  61. package/plugin/sdk/enum/bridge-event.ts +1 -1
  62. package/plugin/sdk/examples/storage-onDidChange-example.js +1 -1
  63. package/plugin/sdk/features.ts +34 -26
  64. package/plugin/sdk/hooks/bridge.ts +3 -6
  65. package/plugin/sdk/hooks/index.ts +1 -1
  66. package/plugin/sdk/hooks/life-cycle.ts +4 -10
  67. package/plugin/sdk/index.ts +9 -13
  68. package/plugin/sdk/service/index.ts +3 -3
  69. package/plugin/sdk/storage.ts +4 -4
  70. package/plugin/sdk/system.ts +1 -1
  71. package/plugin/sdk/types.ts +169 -143
  72. package/plugin/sdk/window/index.ts +8 -5
  73. package/preload/loading.ts +6 -6
  74. package/preload/renderer.ts +4 -2
  75. package/renderer/hooks/arg-mapper.ts +1 -2
  76. package/renderer/hooks/index.ts +2 -0
  77. package/renderer/hooks/initialize.ts +10 -8
  78. package/renderer/hooks/performance.ts +4 -4
  79. package/renderer/hooks/use-channel.ts +150 -0
  80. package/renderer/hooks/use-intelligence.ts +236 -0
  81. package/renderer/index.ts +6 -1
  82. package/renderer/ref.ts +32 -36
  83. package/renderer/slots.ts +29 -26
  84. package/renderer/storage/app-settings.ts +16 -6
  85. package/renderer/storage/base-storage.ts +236 -88
  86. package/renderer/storage/index.ts +3 -0
  87. package/renderer/storage/intelligence-storage.ts +215 -0
  88. package/renderer/storage/openers.ts +13 -3
  89. package/renderer/touch-sdk/env.ts +41 -41
  90. package/renderer/touch-sdk/index.ts +1 -1
  91. package/renderer/touch-sdk/terminal.ts +5 -5
  92. package/renderer/touch-sdk/utils.ts +4 -3
  93. package/search/levenshtein-utils.ts +11 -11
  94. package/search/types.ts +102 -103
  95. package/service/index.ts +11 -11
  96. package/service/protocol/index.ts +217 -14
  97. package/types/division-box.ts +248 -0
  98. package/types/download.ts +72 -34
  99. package/types/icon.ts +2 -1
  100. package/types/index.ts +3 -1
  101. package/types/intelligence.ts +413 -0
  102. package/types/modules/base.ts +16 -16
  103. package/types/modules/index.ts +1 -1
  104. package/types/modules/module-lifecycle.ts +21 -21
  105. package/types/modules/module-manager.ts +11 -11
  106. package/types/modules/module.ts +16 -16
  107. package/types/storage.ts +0 -1
  108. package/types/touch-app-core.ts +32 -32
  109. package/types/update.ts +79 -21
  110. package/core-box/README.md +0 -218
  111. package/core-box/builder/tuff-builder.example.ts.bak +0 -258
  112. package/core-box/run-tests.sh +0 -7
  113. 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) 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,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): 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
+ 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
- export const storages = new Map<string, TouchStorage<any>>();
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
- 'TouchStorage: channel is not initialized. Please call initStorageChannel(...) before using.'
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.#qualifiedName = qName;
96
- this.originalData = initData;
190
+ this.#channelInitialized = true
97
191
 
98
- // const stored = (channel.sendSync('storage:get', qName) as Partial<T>) || {};
99
- this.data = reactive({ ...initData }) as UnwrapNestedRefs<T>;
100
- this.loadFromRemote()
192
+ const result = channel!.sendSync('storage:get', this.#qualifiedName)
193
+ const parsed = result ? (result as Partial<T>) : {}
101
194
 
102
- if (onUpdate) this._onUpdate.push(onUpdate);
195
+ this.assignData(parsed)
103
196
 
104
- channel.regChannel('storage:update', ({ data }) => {
197
+ // Register update listener
198
+ channel!.regChannel('storage:update', ({ data }) => {
105
199
  const { name } = data!
106
200
 
107
- if (name === qName) {
201
+ if (name === this.#qualifiedName) {
108
202
  this.loadFromRemote()
109
203
  }
110
204
  })
111
205
 
112
- storages.set(qName, this);
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("TouchStorage: channel not initialized");
249
+ throw new Error('TouchStorage: channel not initialized')
153
250
  }
154
251
 
155
252
  if (this.#assigning && !options?.force) {
156
- console.debug("[Storage] Skip saveToRemote for", this.getQualifiedName());
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
- 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
- );
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
- setTimeout(() => {
271
- this.#assigning = false;
272
- console.debug(`[Storage] Resume auto-save watch handle for ${this.getQualifiedName()}`);
273
- }, 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 })
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("TouchStorage: channel not initialized");
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
- throw new Error("TouchStorage: channel not initialized");
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
  }
@@ -1 +1,4 @@
1
+ export * from './app-settings'
1
2
  export * from './base-storage'
3
+ export * from './intelligence-storage'
4
+ export * from './openers'