@composy/store-vue 0.0.1 → 0.0.2

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 (251) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +169 -154
  3. package/dist/index.js +151 -53
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +242 -170
  6. package/dist/index.min.js.map +1 -1
  7. package/es/.dts-output-manifest.json +1 -0
  8. package/es/.dts.tsbuildinfo +1 -0
  9. package/es/composables/defineSetupStore.d.ts +0 -1
  10. package/es/composables/defineSetupStore.d.ts.map +1 -1
  11. package/es/composables/defineSetupStore.js +1 -1
  12. package/es/composables/defineSetupStore.js.map +1 -1
  13. package/es/composables/index.d.ts +8 -9
  14. package/es/composables/index.d.ts.map +1 -1
  15. package/es/composables/index.js +1 -1
  16. package/es/composables/useFormStore.d.ts +0 -1
  17. package/es/composables/useFormStore.d.ts.map +1 -1
  18. package/es/composables/useFormStore.js +1 -1
  19. package/es/composables/useFormStore.js.map +1 -1
  20. package/es/composables/useOptimistic.d.ts +1 -52
  21. package/es/composables/useOptimistic.d.ts.map +1 -1
  22. package/es/composables/useOptimistic.js +35 -20
  23. package/es/composables/useOptimistic.js.map +1 -1
  24. package/es/composables/usePersist.d.ts +0 -1
  25. package/es/composables/usePersist.d.ts.map +1 -1
  26. package/es/composables/usePersist.js +16 -7
  27. package/es/composables/usePersist.js.map +1 -1
  28. package/es/composables/usePersistedState.d.ts +0 -1
  29. package/es/composables/usePersistedState.d.ts.map +1 -1
  30. package/es/composables/usePersistedState.js +16 -3
  31. package/es/composables/usePersistedState.js.map +1 -1
  32. package/es/composables/useSharedState.d.ts +0 -1
  33. package/es/composables/useSharedState.d.ts.map +1 -1
  34. package/es/composables/useSharedState.js +25 -8
  35. package/es/composables/useSharedState.js.map +1 -1
  36. package/es/composables/useStore.d.ts +0 -1
  37. package/es/composables/useStore.js +1 -1
  38. package/es/composables/useStore.js.map +1 -1
  39. package/es/composables/useStoreDevtools.d.ts +0 -1
  40. package/es/composables/useStoreDevtools.d.ts.map +1 -1
  41. package/es/composables/useStoreDevtools.js +1 -1
  42. package/es/composables/useStoreDevtools.js.map +1 -1
  43. package/es/composables/useStoreSnapshot.d.ts +0 -1
  44. package/es/composables/useStoreSnapshot.d.ts.map +1 -1
  45. package/es/composables/useStoreSnapshot.js +18 -8
  46. package/es/composables/useStoreSnapshot.js.map +1 -1
  47. package/es/composables/useSubscribe.d.ts +0 -1
  48. package/es/composables/useSubscribe.d.ts.map +1 -1
  49. package/es/composables/useSubscribe.js +1 -1
  50. package/es/composables/useSubscribe.js.map +1 -1
  51. package/es/context/injection-keys.d.ts +0 -1
  52. package/es/context/injection-keys.d.ts.map +1 -1
  53. package/es/context/injection-keys.js +1 -1
  54. package/es/context/injection-keys.js.map +1 -1
  55. package/es/index.d.ts +1 -2
  56. package/es/index.js +2 -2
  57. package/es/index.js.map +1 -1
  58. package/es/plugins/engine-plugin.d.ts +1 -2
  59. package/es/plugins/engine-plugin.d.ts.map +1 -1
  60. package/es/plugins/engine-plugin.js +2 -2
  61. package/es/plugins/engine-plugin.js.map +1 -1
  62. package/es/plugins/enhanced-persist-plugin.d.ts +0 -1
  63. package/es/plugins/enhanced-persist-plugin.js +1 -1
  64. package/es/plugins/index.d.ts +0 -1
  65. package/es/plugins/index.js +1 -1
  66. package/es/plugins/pinia-plugin.d.ts +0 -1
  67. package/es/plugins/pinia-plugin.d.ts.map +1 -1
  68. package/es/plugins/pinia-plugin.js +24 -6
  69. package/es/plugins/pinia-plugin.js.map +1 -1
  70. package/es/plugins/registry-plugin.d.ts +0 -1
  71. package/es/plugins/registry-plugin.js +1 -1
  72. package/es/plugins/vue-plugin.d.ts +0 -1
  73. package/es/plugins/vue-plugin.d.ts.map +1 -1
  74. package/es/plugins/vue-plugin.js +1 -1
  75. package/es/plugins/vue-plugin.js.map +1 -1
  76. package/es/runtime/install-store-app.d.ts +0 -1
  77. package/es/runtime/install-store-app.js +1 -1
  78. package/es/runtime/install-store-app.js.map +1 -1
  79. package/es/runtime/store-plugin-options.d.ts +0 -1
  80. package/es/runtime/store-plugin-options.js +1 -1
  81. package/es/runtime/store-plugin-options.js.map +1 -1
  82. package/es/runtime/store-registry.d.ts +0 -1
  83. package/es/runtime/store-registry.d.ts.map +1 -1
  84. package/es/runtime/store-registry.js +14 -4
  85. package/es/runtime/store-registry.js.map +1 -1
  86. package/es/runtime/store-runtime.d.ts +1 -2
  87. package/es/runtime/store-runtime.d.ts.map +1 -1
  88. package/es/runtime/store-runtime.js +8 -2
  89. package/es/runtime/store-runtime.js.map +1 -1
  90. package/es/types.d.ts +2 -1
  91. package/es/types.d.ts.map +1 -1
  92. package/es/types.js +1 -1
  93. package/esm/composables/defineSetupStore.d.ts.map +1 -1
  94. package/esm/composables/defineSetupStore.js +1 -1
  95. package/esm/composables/defineSetupStore.js.map +1 -1
  96. package/esm/composables/index.d.ts +8 -8
  97. package/esm/composables/index.d.ts.map +1 -1
  98. package/esm/composables/index.js +1 -1
  99. package/esm/composables/useFormStore.d.ts.map +1 -1
  100. package/esm/composables/useFormStore.js +1 -1
  101. package/esm/composables/useFormStore.js.map +1 -1
  102. package/esm/composables/useOptimistic.d.ts +1 -51
  103. package/esm/composables/useOptimistic.d.ts.map +1 -1
  104. package/esm/composables/useOptimistic.js +35 -20
  105. package/esm/composables/useOptimistic.js.map +1 -1
  106. package/esm/composables/usePersist.d.ts.map +1 -1
  107. package/esm/composables/usePersist.js +16 -7
  108. package/esm/composables/usePersist.js.map +1 -1
  109. package/esm/composables/usePersistedState.d.ts.map +1 -1
  110. package/esm/composables/usePersistedState.js +16 -3
  111. package/esm/composables/usePersistedState.js.map +1 -1
  112. package/esm/composables/useSharedState.d.ts.map +1 -1
  113. package/esm/composables/useSharedState.js +25 -8
  114. package/esm/composables/useSharedState.js.map +1 -1
  115. package/esm/composables/useStore.js +1 -1
  116. package/esm/composables/useStore.js.map +1 -1
  117. package/esm/composables/useStoreDevtools.d.ts.map +1 -1
  118. package/esm/composables/useStoreDevtools.js +1 -1
  119. package/esm/composables/useStoreDevtools.js.map +1 -1
  120. package/esm/composables/useStoreSnapshot.d.ts.map +1 -1
  121. package/esm/composables/useStoreSnapshot.js +18 -8
  122. package/esm/composables/useStoreSnapshot.js.map +1 -1
  123. package/esm/composables/useSubscribe.d.ts.map +1 -1
  124. package/esm/composables/useSubscribe.js +1 -1
  125. package/esm/composables/useSubscribe.js.map +1 -1
  126. package/esm/context/injection-keys.d.ts.map +1 -1
  127. package/esm/context/injection-keys.js +1 -1
  128. package/esm/context/injection-keys.js.map +1 -1
  129. package/esm/index.d.ts +1 -1
  130. package/esm/index.js +2 -2
  131. package/esm/index.js.map +1 -1
  132. package/esm/plugins/engine-plugin.d.ts +1 -1
  133. package/esm/plugins/engine-plugin.d.ts.map +1 -1
  134. package/esm/plugins/engine-plugin.js +2 -2
  135. package/esm/plugins/engine-plugin.js.map +1 -1
  136. package/esm/plugins/enhanced-persist-plugin.js +1 -1
  137. package/esm/plugins/index.js +1 -1
  138. package/esm/plugins/pinia-plugin.d.ts.map +1 -1
  139. package/esm/plugins/pinia-plugin.js +24 -6
  140. package/esm/plugins/pinia-plugin.js.map +1 -1
  141. package/esm/plugins/registry-plugin.js +1 -1
  142. package/esm/plugins/vue-plugin.d.ts.map +1 -1
  143. package/esm/plugins/vue-plugin.js +1 -1
  144. package/esm/plugins/vue-plugin.js.map +1 -1
  145. package/esm/runtime/install-store-app.js +1 -1
  146. package/esm/runtime/install-store-app.js.map +1 -1
  147. package/esm/runtime/store-plugin-options.js +1 -1
  148. package/esm/runtime/store-plugin-options.js.map +1 -1
  149. package/esm/runtime/store-registry.d.ts.map +1 -1
  150. package/esm/runtime/store-registry.js +14 -4
  151. package/esm/runtime/store-registry.js.map +1 -1
  152. package/esm/runtime/store-runtime.d.ts +1 -1
  153. package/esm/runtime/store-runtime.d.ts.map +1 -1
  154. package/esm/runtime/store-runtime.js +8 -2
  155. package/esm/runtime/store-runtime.js.map +1 -1
  156. package/esm/types.d.ts +2 -0
  157. package/esm/types.d.ts.map +1 -1
  158. package/esm/types.js +1 -1
  159. package/lib/composables/defineSetupStore.cjs +1 -1
  160. package/lib/composables/defineSetupStore.cjs.map +1 -1
  161. package/lib/composables/{defineSetupStore.d.ts → defineSetupStore.d.cts} +0 -1
  162. package/lib/composables/defineSetupStore.d.cts.map +1 -0
  163. package/lib/composables/index.cjs +1 -1
  164. package/lib/composables/{index.d.ts → index.d.cts} +8 -9
  165. package/lib/composables/index.d.cts.map +1 -0
  166. package/lib/composables/useFormStore.cjs +1 -1
  167. package/lib/composables/useFormStore.cjs.map +1 -1
  168. package/lib/composables/{useFormStore.d.ts → useFormStore.d.cts} +0 -1
  169. package/lib/composables/useFormStore.d.cts.map +1 -0
  170. package/lib/composables/useOptimistic.cjs +35 -20
  171. package/lib/composables/useOptimistic.cjs.map +1 -1
  172. package/lib/composables/useOptimistic.d.cts +16 -0
  173. package/lib/composables/useOptimistic.d.cts.map +1 -0
  174. package/lib/composables/usePersist.cjs +16 -7
  175. package/lib/composables/usePersist.cjs.map +1 -1
  176. package/lib/composables/{usePersist.d.ts → usePersist.d.cts} +0 -1
  177. package/lib/composables/usePersist.d.cts.map +1 -0
  178. package/lib/composables/usePersistedState.cjs +16 -3
  179. package/lib/composables/usePersistedState.cjs.map +1 -1
  180. package/lib/composables/{usePersistedState.d.ts → usePersistedState.d.cts} +0 -1
  181. package/lib/composables/usePersistedState.d.cts.map +1 -0
  182. package/lib/composables/useSharedState.cjs +24 -7
  183. package/lib/composables/useSharedState.cjs.map +1 -1
  184. package/lib/composables/{useSharedState.d.ts → useSharedState.d.cts} +0 -1
  185. package/lib/composables/useSharedState.d.cts.map +1 -0
  186. package/lib/composables/useStore.cjs +1 -1
  187. package/lib/composables/useStore.cjs.map +1 -1
  188. package/lib/composables/{useStore.d.ts → useStore.d.cts} +0 -1
  189. package/lib/composables/useStore.d.cts.map +1 -0
  190. package/lib/composables/useStoreDevtools.cjs +1 -1
  191. package/lib/composables/useStoreDevtools.cjs.map +1 -1
  192. package/lib/composables/{useStoreDevtools.d.ts → useStoreDevtools.d.cts} +0 -1
  193. package/lib/composables/useStoreDevtools.d.cts.map +1 -0
  194. package/lib/composables/useStoreSnapshot.cjs +18 -8
  195. package/lib/composables/useStoreSnapshot.cjs.map +1 -1
  196. package/lib/composables/{useStoreSnapshot.d.ts → useStoreSnapshot.d.cts} +0 -1
  197. package/lib/composables/useStoreSnapshot.d.cts.map +1 -0
  198. package/lib/composables/useSubscribe.cjs +1 -1
  199. package/lib/composables/useSubscribe.cjs.map +1 -1
  200. package/lib/composables/{useSubscribe.d.ts → useSubscribe.d.cts} +0 -1
  201. package/lib/composables/useSubscribe.d.cts.map +1 -0
  202. package/lib/context/injection-keys.cjs +1 -1
  203. package/lib/context/injection-keys.cjs.map +1 -1
  204. package/lib/context/{injection-keys.d.ts → injection-keys.d.cts} +0 -1
  205. package/lib/context/injection-keys.d.cts.map +1 -0
  206. package/lib/index.cjs +2 -2
  207. package/lib/index.cjs.map +1 -1
  208. package/lib/{index.d.ts → index.d.cts} +1 -2
  209. package/lib/index.d.cts.map +1 -0
  210. package/lib/plugins/engine-plugin.cjs +2 -2
  211. package/lib/plugins/engine-plugin.cjs.map +1 -1
  212. package/lib/plugins/{engine-plugin.d.ts → engine-plugin.d.cts} +1 -2
  213. package/lib/plugins/engine-plugin.d.cts.map +1 -0
  214. package/lib/plugins/enhanced-persist-plugin.cjs +1 -1
  215. package/lib/plugins/{enhanced-persist-plugin.d.ts → enhanced-persist-plugin.d.cts} +0 -1
  216. package/lib/plugins/enhanced-persist-plugin.d.cts.map +1 -0
  217. package/lib/plugins/index.cjs +1 -1
  218. package/lib/plugins/{index.d.ts → index.d.cts} +0 -1
  219. package/lib/plugins/index.d.cts.map +1 -0
  220. package/lib/plugins/pinia-plugin.cjs +24 -6
  221. package/lib/plugins/pinia-plugin.cjs.map +1 -1
  222. package/lib/plugins/{pinia-plugin.d.ts → pinia-plugin.d.cts} +0 -1
  223. package/lib/plugins/pinia-plugin.d.cts.map +1 -0
  224. package/lib/plugins/registry-plugin.cjs +1 -1
  225. package/lib/plugins/{registry-plugin.d.ts → registry-plugin.d.cts} +0 -1
  226. package/lib/plugins/registry-plugin.d.cts.map +1 -0
  227. package/lib/plugins/vue-plugin.cjs +1 -1
  228. package/lib/plugins/vue-plugin.cjs.map +1 -1
  229. package/lib/plugins/{vue-plugin.d.ts → vue-plugin.d.cts} +0 -1
  230. package/lib/plugins/vue-plugin.d.cts.map +1 -0
  231. package/lib/runtime/install-store-app.cjs +1 -1
  232. package/lib/runtime/install-store-app.cjs.map +1 -1
  233. package/lib/runtime/{install-store-app.d.ts → install-store-app.d.cts} +0 -1
  234. package/lib/runtime/install-store-app.d.cts.map +1 -0
  235. package/lib/runtime/store-plugin-options.cjs +1 -1
  236. package/lib/runtime/store-plugin-options.cjs.map +1 -1
  237. package/lib/runtime/{store-plugin-options.d.ts → store-plugin-options.d.cts} +0 -1
  238. package/lib/runtime/store-plugin-options.d.cts.map +1 -0
  239. package/lib/runtime/store-registry.cjs +14 -4
  240. package/lib/runtime/store-registry.cjs.map +1 -1
  241. package/lib/runtime/{store-registry.d.ts → store-registry.d.cts} +0 -1
  242. package/lib/runtime/store-registry.d.cts.map +1 -0
  243. package/lib/runtime/store-runtime.cjs +8 -2
  244. package/lib/runtime/store-runtime.cjs.map +1 -1
  245. package/lib/runtime/{store-runtime.d.ts → store-runtime.d.cts} +1 -2
  246. package/lib/runtime/store-runtime.d.cts.map +1 -0
  247. package/lib/types.cjs +1 -1
  248. package/lib/{types.d.ts → types.d.cts} +2 -1
  249. package/lib/types.d.cts.map +1 -0
  250. package/package.json +19 -16
  251. package/lib/composables/useOptimistic.d.ts +0 -67
@@ -1 +1 @@
1
- {"version":3,"file":"usePersistedState.cjs","names":[],"sources":["../../src/composables/usePersistedState.ts"],"sourcesContent":["/* ============================================================\n * usePersistedState.ts\n * 持久化状态 Composable\n * 提供将 Vue 响应式状态自动持久化到 localStorage/sessionStorage 的能力\n * 支持 TTL 过期、跨标签页同步、防抖保存、序列化自定义等特性\n * ============================================================ */\n\n/* 从 Vue 导入 Ref 类型 */\nimport type {Ref} from 'vue';\n/* 从 store-core 导入防抖和深拷贝工具函数 */\nimport { debounce, deepClone } from '@composy/store-core'\n/* 从 Vue 导入生命周期钩子和响应式 API */\nimport { onUnmounted, ref, watch } from 'vue'\n\n/**\n * 持久化状态配置选项接口\n * @property key - 存储键名\n * @property storage - 存储类型,默认 'localStorage'\n * @property defaultValue - 默认值(当存储中无数据或过期时使用)\n * @serializer - 自定义序列化函数(默认 JSON.stringify)\n * @deserializer - 自定义反序列化函数(默认 JSON.parse)\n * @property syncTabs - 是否同步其他标签页的变化\n * @property debounce - 防抖延迟(毫秒),0 表示不防抖\n * @property deep - 是否深度监听状态变化\n * @property ttl - 存活时间(毫秒),过期后自动清除\n * @property onBeforeChange - 值变化前的钩子,返回 false 可阻止保存\n * @property onAfterChange - 值变化后的钩子\n * @property onBeforeRestore - 恢复前的钩子,可转换恢复的值\n * @property onAfterRestore - 恢复后的钩子\n */\nexport interface PersistedStateOptions<T> {\n key: string\n storage?: 'localStorage' | 'sessionStorage'\n defaultValue?: T\n serializer?: (value: T) => string\n deserializer?: (value: string) => T\n syncTabs?: boolean\n debounce?: number\n deep?: boolean\n ttl?: number\n onBeforeChange?: (newValue: T, oldValue: T) => boolean | void\n onAfterChange?: (newValue: T, oldValue: T) => void\n onBeforeRestore?: (value: T) => T\n onAfterRestore?: (value: T) => void\n}\n\n/**\n * 持久化状态返回值接口\n * @property state - 响应式状态引用\n * @property save - 手动保存到存储\n * @property restore - 从存储手动恢复\n * @property clear - 清除存储中的数据\n * @property reset - 重置为默认值并清除存储\n * @property isExpired - 检查存储数据是否已过期\n */\nexport interface PersistedStateReturn<T> {\n state: Ref<T>\n save: () => void\n restore: () => void\n clear: () => void\n reset: () => void\n isExpired: () => boolean\n}\n\n/**\n * 存储项的内部格式接口\n * 包装序列化后的值,附加时间戳和过期时间\n * @property value - 序列化后的字符串值\n * @property timestamp - 保存时间戳\n * @property expiresAt - 过期时间戳(可选,无则永不过期)\n */\ninterface StorageItem {\n value: string\n timestamp: number\n expiresAt?: number\n}\n\n/**\n * 默认序列化函数\n * 使用 JSON.stringify 将值转为 JSON 字符串\n */\nfunction defaultSerializer<T>(value: T): string {\n return JSON.stringify(value)\n}\n\n/**\n * 默认反序列化函数\n * 使用 JSON.parse 解析 JSON 字符串,解析失败则原样返回\n */\nfunction defaultDeserializer<T>(value: string): T {\n try {\n return JSON.parse(value)\n }\n catch {\n /* 解析失败(数据损坏),原样返回 */\n return value as T\n }\n}\n\n/**\n * 获取浏览器存储对象\n * 在非浏览器环境(如 SSR)返回 null\n * @param type - 存储类型\n * @returns Storage 对象或 null\n */\nfunction getStorage(type: 'localStorage' | 'sessionStorage'): Storage | null {\n /* SSR 环境下无 window 对象 */\n if (typeof window === 'undefined') {\n return null\n }\n\n return type === 'localStorage' ? window.localStorage : window.sessionStorage\n}\n\n/**\n * 持久化状态 Composable\n * 将 Vue 响应式状态自动持久化到浏览器存储\n * 支持 TTL 过期、跨标签页同步、防抖、生命周期钩子\n *\n * @param options - 配置选项\n * @returns 包含状态和操作方法的对象\n */\nexport function usePersistedState<T>(\n options: PersistedStateOptions<T>,\n): PersistedStateReturn<T> {\n /* 解构配置选项,设置默认值 */\n const {\n key, /* 存储键名 */\n storage: storageType = 'localStorage', /* 存储类型,默认 localStorage */\n defaultValue, /* 默认值 */\n serializer = defaultSerializer, /* 序列化函数 */\n deserializer = defaultDeserializer, /* 反序列化函数 */\n syncTabs = false, /* 是否跨标签页同步 */\n debounce: debounceDelay = 0, /* 防抖延迟 */\n deep = true, /* 是否深度监听 */\n ttl, /* 过期时间 */\n onBeforeChange, /* 变化前钩子 */\n onAfterChange, /* 变化后钩子 */\n onBeforeRestore, /* 恢复前钩子 */\n onAfterRestore, /* 恢复后钩子 */\n } = options\n\n /* 获取实际的存储对象 */\n const storage = getStorage(storageType)\n /* 创建默认值的深拷贝工厂函数(避免共享引用) */\n const createDefaultValue = (): T => deepClone(defaultValue as T)\n /* 创建响应式状态,初始值为默认值的深拷贝 */\n const state = ref(createDefaultValue()) as Ref<T>\n\n /**\n * 将当前状态保存到存储\n * 将状态序列化并包装为 StorageItem 格式(含时间戳和过期时间)\n */\n const save = (): void => {\n if (!storage) {\n return /* 存储不可用(SSR 环境) */\n }\n\n try {\n /* 构建存储项:包含序列化值、时间戳和可选的过期时间 */\n const item: StorageItem = {\n value: serializer(state.value), /* 序列化当前状态 */\n timestamp: Date.now(), /* 保存时间 */\n expiresAt: ttl ? Date.now() + ttl : undefined, /* 过期时间 */\n }\n\n /* 将存储项 JSON 序列化后写入存储 */\n storage.setItem(key, JSON.stringify(item))\n }\n catch (error) {\n /* 存储可能已满或被禁用 */\n console.error(`[usePersistedState] 保存失败 (${key}):`, error)\n }\n }\n\n /**\n * 清除存储中的数据\n */\n const clear = (): void => {\n if (!storage) {\n return\n }\n\n try {\n storage.removeItem(key)\n }\n catch (error) {\n console.error(`[usePersistedState] 清除失败 (${key}):`, error)\n }\n }\n\n /**\n * 从存储恢复状态\n * 读取存储项,检查过期,执行恢复前钩子,更新响应式状态\n */\n const restore = (): void => {\n if (!storage) {\n return\n }\n\n try {\n /* 读取存储项 */\n const stored = storage.getItem(key)\n if (!stored) {\n return /* 存储中无数据 */\n }\n\n /* 解析存储项 */\n const item = JSON.parse(stored) as StorageItem\n /* 检查是否过期,过期则清除并退出 */\n if (item.expiresAt && Date.now() > item.expiresAt) {\n clear()\n return\n }\n\n /* 反序列化存储的值 */\n let value = deserializer(item.value)\n /* 执行恢复前钩子,允许转换恢复的值 */\n if (onBeforeRestore) {\n value = onBeforeRestore(value)\n }\n\n /* 更新响应式状态 */\n state.value = value\n /* 执行恢复后钩子 */\n onAfterRestore?.(value)\n }\n catch (error) {\n console.error(`[usePersistedState] 恢复失败 (${key}):`, error)\n }\n }\n\n /**\n * 重置为默认值并清除存储\n */\n const reset = (): void => {\n state.value = createDefaultValue() /* 恢复为默认值 */\n clear() /* 清除存储 */\n }\n\n /**\n * 检查存储数据是否已过期\n * @returns 是否已过期\n */\n const isExpired = (): boolean => {\n /* 无存储或无 TTL 则不可能过期 */\n if (!storage || !ttl) {\n return false\n }\n\n try {\n const stored = storage.getItem(key)\n if (!stored) {\n return false /* 无数据,视为未过期 */\n }\n\n const item = JSON.parse(stored) as StorageItem\n /* 比较当前时间与过期时间 */\n return item.expiresAt ? Date.now() > item.expiresAt : false\n }\n catch {\n return false\n }\n }\n\n /* 如果设置了防抖延迟,创建防抖版的 save 函数 */\n const debouncedSave = debounceDelay > 0 ? debounce(save, debounceDelay) : null\n\n /**\n * 处理跨标签页存储事件\n * 当其他标签页修改了同一 key 的存储时触发\n */\n const handleStorageEvent = (event: StorageEvent): void => {\n /* 只关注目标 key 且有新值的事件 */\n if (event.key !== key || !event.newValue) {\n return\n }\n\n try {\n const item = JSON.parse(event.newValue) as StorageItem\n /* 跳过已过期的数据 */\n if (item.expiresAt && Date.now() > item.expiresAt) {\n return\n }\n\n /* 反序列化并更新本地状态 */\n state.value = deserializer(item.value)\n }\n catch (error) {\n console.error(`[usePersistedState] 同步失败 (${key}):`, error)\n }\n }\n\n /* ── 初始化 ── */\n /* 组件挂载时立即尝试从存储恢复状态 */\n restore()\n\n /* ── 状态变化监听 ── */\n /* 监听 state 变化,自动保存到存储 */\n watch(\n state,\n (newValue, oldValue) => {\n /* 执行变化前钩子,返回 false 可阻止保存 */\n const canContinue = onBeforeChange?.(newValue, oldValue)\n if (canContinue === false) {\n return /* 阻止保存 */\n }\n\n /* 使用防抖保存或立即保存 */\n if (debouncedSave) {\n debouncedSave()\n }\n else {\n save()\n }\n\n /* 执行变化后钩子 */\n onAfterChange?.(newValue, oldValue)\n },\n { deep }, /* 深度监听选项 */\n )\n\n /* ── 跨标签页同步 ── */\n /* 如果启用了跨标签页同步,监听 storage 事件 */\n if (syncTabs && typeof window !== 'undefined') {\n window.addEventListener('storage', handleStorageEvent)\n }\n\n /* ── 清理 ── */\n /* 组件卸载时取消防抖和移除事件监听 */\n onUnmounted(() => {\n /* 取消待执行的防抖保存 */\n debouncedSave?.cancel()\n /* 移除跨标签页同步的事件监听 */\n if (syncTabs && typeof window !== 'undefined') {\n window.removeEventListener('storage', handleStorageEvent)\n }\n })\n\n /* ── 返回状态和方法 ── */\n return {\n state, /* 响应式状态 */\n save, /* 手动保存 */\n restore, /* 手动恢复 */\n clear, /* 清除存储 */\n reset, /* 重置为默认值 */\n isExpired, /* 检查是否过期 */\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"usePersistedState.cjs","names":[],"sources":["../../src/composables/usePersistedState.ts"],"sourcesContent":["/* ============================================================\r\n * usePersistedState.ts\r\n * 持久化状态 Composable\r\n * 提供将 Vue 响应式状态自动持久化到 localStorage/sessionStorage 的能力\r\n * 支持 TTL 过期、跨标签页同步、防抖保存、序列化自定义等特性\r\n * ============================================================ */\r\n\r\n/* 从 Vue 导入 Ref 类型 */\r\nimport type { Ref } from 'vue'\r\n/* 从 store-core 导入防抖和深拷贝工具函数 */\r\nimport { debounce, deepClone } from '@composy/store-core'\r\n/* 从 Vue 导入生命周期钩子和响应式 API */\r\nimport { onUnmounted, ref, watch } from 'vue'\r\n\r\n/**\r\n * 持久化状态配置选项接口\r\n * @property key - 存储键名\r\n * @property storage - 存储类型,默认 'localStorage'\r\n * @property defaultValue - 默认值(当存储中无数据或过期时使用)\r\n * @serializer - 自定义序列化函数(默认 JSON.stringify)\r\n * @deserializer - 自定义反序列化函数(默认 JSON.parse)\r\n * @property syncTabs - 是否同步其他标签页的变化\r\n * @property debounce - 防抖延迟(毫秒),0 表示不防抖\r\n * @property deep - 是否深度监听状态变化\r\n * @property ttl - 存活时间(毫秒),过期后自动清除\r\n * @property onBeforeChange - 值变化前的钩子,返回 false 可阻止保存\r\n * @property onAfterChange - 值变化后的钩子\r\n * @property onBeforeRestore - 恢复前的钩子,可转换恢复的值\r\n * @property onAfterRestore - 恢复后的钩子\r\n */\r\nexport interface PersistedStateOptions<T> {\r\n key: string\r\n storage?: 'localStorage' | 'sessionStorage'\r\n defaultValue?: T\r\n serializer?: (value: T) => string\r\n deserializer?: (value: string) => T\r\n syncTabs?: boolean\r\n debounce?: number\r\n deep?: boolean\r\n ttl?: number\r\n onBeforeChange?: (newValue: T, oldValue: T) => boolean | void\r\n onAfterChange?: (newValue: T, oldValue: T) => void\r\n onBeforeRestore?: (value: T) => T\r\n onAfterRestore?: (value: T) => void\r\n}\r\n\r\n/**\r\n * 持久化状态返回值接口\r\n * @property state - 响应式状态引用\r\n * @property save - 手动保存到存储\r\n * @property restore - 从存储手动恢复\r\n * @property clear - 清除存储中的数据\r\n * @property reset - 重置为默认值并清除存储\r\n * @property isExpired - 检查存储数据是否已过期\r\n */\r\nexport interface PersistedStateReturn<T> {\r\n state: Ref<T>\r\n save: () => void\r\n restore: () => void\r\n clear: () => void\r\n reset: () => void\r\n isExpired: () => boolean\r\n}\r\n\r\n/**\r\n * 存储项的内部格式接口\r\n * 包装序列化后的值,附加时间戳和过期时间\r\n * @property value - 序列化后的字符串值\r\n * @property timestamp - 保存时间戳\r\n * @property expiresAt - 过期时间戳(可选,无则永不过期)\r\n */\r\ninterface StorageItem {\r\n value: string\r\n timestamp: number\r\n expiresAt?: number\r\n}\r\n\r\n/**\r\n * 默认序列化函数\r\n * 使用 JSON.stringify 将值转为 JSON 字符串\r\n */\r\nfunction defaultSerializer<T>(value: T): string {\r\n return JSON.stringify(value)\r\n}\r\n\r\n/**\r\n * 默认反序列化函数\r\n * 使用 JSON.parse 解析 JSON 字符串,解析失败则原样返回\r\n */\r\nfunction defaultDeserializer<T>(value: string): T {\r\n try {\r\n return JSON.parse(value)\r\n } catch {\r\n /* 解析失败(数据损坏),原样返回 */\r\n return value as T\r\n }\r\n}\r\n\r\n/**\r\n * 获取浏览器存储对象\r\n * 在非浏览器环境(如 SSR)返回 null\r\n * @param type - 存储类型\r\n * @returns Storage 对象或 null\r\n */\r\nfunction getStorage(type: 'localStorage' | 'sessionStorage'): Storage | null {\r\n /* SSR 环境下无 window 对象 */\r\n if (typeof window === 'undefined') {\r\n return null\r\n }\r\n\r\n return type === 'localStorage' ? window.localStorage : window.sessionStorage\r\n}\r\n\r\n/**\r\n * 持久化状态 Composable\r\n * 将 Vue 响应式状态自动持久化到浏览器存储\r\n * 支持 TTL 过期、跨标签页同步、防抖、生命周期钩子\r\n *\r\n * @param options - 配置选项\r\n * @returns 包含状态和操作方法的对象\r\n */\r\nexport function usePersistedState<T>(\r\n options: PersistedStateOptions<T>\r\n): PersistedStateReturn<T> {\r\n /* 解构配置选项,设置默认值 */\r\n const {\r\n key /* 存储键名 */,\r\n storage: storageType = 'localStorage' /* 存储类型,默认 localStorage */,\r\n defaultValue /* 默认值 */,\r\n serializer = defaultSerializer /* 序列化函数 */,\r\n deserializer = defaultDeserializer /* 反序列化函数 */,\r\n syncTabs = false /* 是否跨标签页同步 */,\r\n debounce: debounceDelay = 0 /* 防抖延迟 */,\r\n deep = true /* 是否深度监听 */,\r\n ttl /* 过期时间 */,\r\n onBeforeChange /* 变化前钩子 */,\r\n onAfterChange /* 变化后钩子 */,\r\n onBeforeRestore /* 恢复前钩子 */,\r\n onAfterRestore /* 恢复后钩子 */,\r\n } = options\r\n\r\n /* 获取实际的存储对象 */\r\n const storage = getStorage(storageType)\r\n /* 创建默认值的深拷贝工厂函数(避免共享引用) */\r\n const createDefaultValue = (): T => deepClone(defaultValue as T)\r\n /* 创建响应式状态,初始值为默认值的深拷贝 */\r\n const state = ref(createDefaultValue()) as Ref<T>\r\n /* 标记一次不应回写存储的内部状态同步 */\r\n let skipNextSave = false\r\n\r\n /**\r\n * 将当前状态保存到存储\r\n * 将状态序列化并包装为 StorageItem 格式(含时间戳和过期时间)\r\n */\r\n const save = (): void => {\r\n if (!storage) {\r\n return /* 存储不可用(SSR 环境) */\r\n }\r\n\r\n try {\r\n /* 构建存储项:包含序列化值、时间戳和可选的过期时间 */\r\n const item: StorageItem = {\r\n value: serializer(state.value) /* 序列化当前状态 */,\r\n timestamp: Date.now() /* 保存时间 */,\r\n expiresAt: ttl ? Date.now() + ttl : undefined /* 过期时间 */,\r\n }\r\n\r\n /* 将存储项 JSON 序列化后写入存储 */\r\n storage.setItem(key, JSON.stringify(item))\r\n } catch (error) {\r\n /* 存储可能已满或被禁用 */\r\n console.error(`[usePersistedState] 保存失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n /* 如果设置了防抖延迟,创建防抖版的 save 函数 */\r\n const debouncedSave = debounceDelay > 0 ? debounce(save, debounceDelay) : null\r\n\r\n /**\r\n * 清除存储中的数据\r\n */\r\n const clear = (): void => {\r\n if (!storage) {\r\n return\r\n }\r\n\r\n try {\r\n storage.removeItem(key)\r\n } catch (error) {\r\n console.error(`[usePersistedState] 清除失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n /**\r\n * 从存储恢复状态\r\n * 读取存储项,检查过期,执行恢复前钩子,更新响应式状态\r\n */\r\n const restore = (): void => {\r\n if (!storage) {\r\n return\r\n }\r\n\r\n try {\r\n /* 读取存储项 */\r\n const stored = storage.getItem(key)\r\n if (!stored) {\r\n return /* 存储中无数据 */\r\n }\r\n\r\n /* 解析存储项 */\r\n const item = JSON.parse(stored) as StorageItem\r\n /* 检查是否过期,过期则清除并退出 */\r\n if (item.expiresAt && Date.now() > item.expiresAt) {\r\n clear()\r\n return\r\n }\r\n\r\n /* 反序列化存储的值 */\r\n let value = deserializer(item.value)\r\n /* 执行恢复前钩子,允许转换恢复的值 */\r\n if (onBeforeRestore) {\r\n value = onBeforeRestore(value)\r\n }\r\n\r\n /* 更新响应式状态 */\r\n state.value = value\r\n /* 执行恢复后钩子 */\r\n onAfterRestore?.(value)\r\n } catch (error) {\r\n console.error(`[usePersistedState] 恢复失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n /**\r\n * 重置为默认值并清除存储\r\n */\r\n const reset = (): void => {\r\n debouncedSave?.cancel()\r\n skipNextSave = true\r\n state.value = createDefaultValue() /* 恢复为默认值 */\r\n clear() /* 清除存储 */\r\n }\r\n\r\n /**\r\n * 检查存储数据是否已过期\r\n * @returns 是否已过期\r\n */\r\n const isExpired = (): boolean => {\r\n /* 无存储或无 TTL 则不可能过期 */\r\n if (!storage || !ttl) {\r\n return false\r\n }\r\n\r\n try {\r\n const stored = storage.getItem(key)\r\n if (!stored) {\r\n return false /* 无数据,视为未过期 */\r\n }\r\n\r\n const item = JSON.parse(stored) as StorageItem\r\n /* 比较当前时间与过期时间 */\r\n return item.expiresAt ? Date.now() > item.expiresAt : false\r\n } catch {\r\n return false\r\n }\r\n }\r\n\r\n /**\r\n * 处理跨标签页存储事件\r\n * 当其他标签页修改了同一 key 的存储时触发\r\n */\r\n const handleStorageEvent = (event: StorageEvent): void => {\r\n /* 只关注目标 key */\r\n if (event.key !== key) {\r\n return\r\n }\r\n\r\n /* 其他标签页删除 key 时同步恢复默认值,且不把默认值重新写回去 */\r\n if (!event.newValue) {\r\n debouncedSave?.cancel()\r\n skipNextSave = true\r\n state.value = createDefaultValue()\r\n return\r\n }\r\n\r\n try {\r\n const item = JSON.parse(event.newValue) as StorageItem\r\n /* 跳过已过期的数据 */\r\n if (item.expiresAt && Date.now() > item.expiresAt) {\r\n return\r\n }\r\n\r\n /* 反序列化并更新本地状态 */\r\n state.value = deserializer(item.value)\r\n } catch (error) {\r\n console.error(`[usePersistedState] 同步失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n /* ── 初始化 ── */\r\n /* 组件挂载时立即尝试从存储恢复状态 */\r\n restore()\r\n\r\n /* ── 状态变化监听 ── */\r\n /* 监听 state 变化,自动保存到存储 */\r\n watch(\r\n state,\r\n (newValue, oldValue) => {\r\n if (skipNextSave) {\r\n skipNextSave = false\r\n return\r\n }\r\n\r\n /* 执行变化前钩子,返回 false 可阻止保存 */\r\n const canContinue = onBeforeChange?.(newValue, oldValue)\r\n if (canContinue === false) {\r\n return /* 阻止保存 */\r\n }\r\n\r\n /* 使用防抖保存或立即保存 */\r\n if (debouncedSave) {\r\n debouncedSave()\r\n } else {\r\n save()\r\n }\r\n\r\n /* 执行变化后钩子 */\r\n onAfterChange?.(newValue, oldValue)\r\n },\r\n { deep } /* 深度监听选项 */\r\n )\r\n\r\n /* ── 跨标签页同步 ── */\r\n /* 如果启用了跨标签页同步,监听 storage 事件 */\r\n if (syncTabs && typeof window !== 'undefined') {\r\n window.addEventListener('storage', handleStorageEvent)\r\n }\r\n\r\n /* ── 清理 ── */\r\n /* 组件卸载时取消防抖和移除事件监听 */\r\n onUnmounted(() => {\r\n /* 取消待执行的防抖保存 */\r\n debouncedSave?.cancel()\r\n /* 移除跨标签页同步的事件监听 */\r\n if (syncTabs && typeof window !== 'undefined') {\r\n window.removeEventListener('storage', handleStorageEvent)\r\n }\r\n })\r\n\r\n /* ── 返回状态和方法 ── */\r\n return {\r\n state /* 响应式状态 */,\r\n save /* 手动保存 */,\r\n restore /* 手动恢复 */,\r\n clear /* 清除存储 */,\r\n reset /* 重置为默认值 */,\r\n isExpired /* 检查是否过期 */,\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -56,4 +56,3 @@ export interface PersistedStateReturn<T> {
56
56
  * @returns 包含状态和操作方法的对象
57
57
  */
58
58
  export declare function usePersistedState<T>(options: PersistedStateOptions<T>): PersistedStateReturn<T>;
59
- //# sourceMappingURL=usePersistedState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePersistedState.d.cts","sourceRoot":"","sources":["../../src/composables/usePersistedState.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAM9B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,cAAc,GAAG,gBAAgB,CAAA;IAC3C,YAAY,CAAC,EAAE,CAAC,CAAA;IAChB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAA;IACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAA;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,OAAO,GAAG,IAAI,CAAA;IAC7D,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;IAClD,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;IACjC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;CACpC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IACb,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,SAAS,EAAE,MAAM,OAAO,CAAA;CACzB;AAmDD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAChC,oBAAoB,CAAC,CAAC,CAAC,CA2OzB"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/store-vue v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:39:29 *
5
+ * Build time: 2024-10-12 17:40:04 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -18,7 +18,11 @@ function useSharedState(channelName, initialState, options = {}) {
18
18
  const peerCount = (0, vue.ref)(0);
19
19
  const lastSyncAt = (0, vue.ref)(null);
20
20
  let channel = null;
21
- let isReceiving = false;
21
+ const originId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
22
+ let revision = 0;
23
+ const latestRemoteRevision = /* @__PURE__ */ new Map();
24
+ let pendingRemoteState;
25
+ let hasPendingRemoteState = false;
22
26
  if (typeof BroadcastChannel === "undefined") {
23
27
  console.warn("[useSharedState] BroadcastChannel not supported in this environment");
24
28
  return {
@@ -47,7 +51,9 @@ function useSharedState(channelName, initialState, options = {}) {
47
51
  channel.postMessage({
48
52
  type: "state-update",
49
53
  payload: (0, _composy_store_core.deepClone)(state.value),
50
- timestamp: Date.now()
54
+ timestamp: Date.now(),
55
+ origin: originId,
56
+ revision: ++revision
51
57
  });
52
58
  } catch (e) {
53
59
  console.warn("[useSharedState] broadcast error:", e);
@@ -57,15 +63,26 @@ function useSharedState(channelName, initialState, options = {}) {
57
63
  const msg = event.data;
58
64
  if (!msg || typeof msg !== "object") return;
59
65
  if (msg.type === "state-update") {
60
- isReceiving = true;
61
- state.value = msg.payload;
66
+ if (msg.origin === originId) return;
67
+ const remoteRevision = typeof msg.revision === "number" ? msg.revision : 0;
68
+ if (remoteRevision <= (latestRemoteRevision.get(String(msg.origin)) ?? -1)) return;
69
+ latestRemoteRevision.set(String(msg.origin), remoteRevision);
70
+ pendingRemoteState = (0, _composy_store_core.deepClone)(msg.payload);
71
+ hasPendingRemoteState = true;
72
+ state.value = pendingRemoteState;
62
73
  lastSyncAt.value = msg.timestamp || Date.now();
63
- isReceiving = false;
64
74
  } else if (msg.type === "ping") channel?.postMessage({ type: "pong" });
65
75
  else if (msg.type === "pong") peerCount.value++;
66
76
  };
67
77
  const stopWatch = (0, vue.watch)(state, () => {
68
- if (!isReceiving) broadcast();
78
+ if (hasPendingRemoteState) {
79
+ if ((0, _composy_store_core.deepEqual)(state.value, pendingRemoteState)) {
80
+ hasPendingRemoteState = false;
81
+ return;
82
+ }
83
+ hasPendingRemoteState = false;
84
+ }
85
+ broadcast();
69
86
  }, { deep });
70
87
  function close() {
71
88
  stopWatch();
@@ -1 +1 @@
1
- {"version":3,"file":"useSharedState.cjs","names":[],"sources":["../../src/composables/useSharedState.ts"],"sourcesContent":["/**\n * useSharedState - 跨标签页状态同步\n *\n * 基于 BroadcastChannel API 实现多标签页实时状态同步\n * @module composables/useSharedState\n */\n\nimport type {Ref} from 'vue';\nimport { deepClone } from '@composy/store-core'\nimport { onUnmounted, ref, watch } from 'vue'\n\n/**\n * useSharedState 选项\n */\nexport interface UseSharedStateOptions {\n /** 是否立即将当前值广播给其他标签页 */\n immediate?: boolean\n /** 是否深度监听变化 */\n deep?: boolean\n}\n\n/**\n * useSharedState 返回值\n */\nexport interface UseSharedStateReturn<T> {\n /** 响应式状态 (双向同步) */\n state: Ref<T>\n /** 手动广播当前状态到其他标签页 */\n broadcast: () => void\n /** 连接的标签页数量 (近似值) */\n peerCount: Ref<number>\n /** 最后同步时间 */\n lastSyncAt: Ref<number | null>\n /** 关闭通道 */\n close: () => void\n}\n\n/**\n * 跨标签页状态同步 Hook\n *\n * 使用 BroadcastChannel API 在多个标签页之间实时同步状态。\n * 任意标签页修改状态后,其他标签页会自动收到更新。\n *\n * @template T - 状态类型 (必须可序列化)\n * @param channelName - 通道名称 (相同名称的标签页共享状态)\n * @param initialState - 初始状态\n * @param options - 选项\n * @returns 同步状态和控制 API\n *\n * @example\n * ```typescript\n * // 标签页 A\n * const { state } = useSharedState('my-channel', { count: 0, user: 'Alice' })\n * state.value.count++ // 所有标签页都会收到更新\n *\n * // 标签页 B (相同代码)\n * const { state } = useSharedState('my-channel', { count: 0, user: 'Alice' })\n * // state.value.count 会自动变为 1\n *\n * // 手动广播\n * broadcast()\n *\n * // 关闭通道\n * close()\n * ```\n */\nexport function useSharedState<T>(\n channelName: string,\n initialState: T,\n options: UseSharedStateOptions = {},\n): UseSharedStateReturn<T> {\n const { immediate = false, deep = true } = options\n\n const state = ref<T>(deepClone(initialState)) as Ref<T>\n const peerCount = ref(0)\n const lastSyncAt = ref<number | null>(null)\n\n let channel: BroadcastChannel | null = null\n let isReceiving = false\n\n // 检测 BroadcastChannel 是否可用\n if (typeof BroadcastChannel === 'undefined') {\n console.warn('[useSharedState] BroadcastChannel not supported in this environment')\n return {\n state,\n broadcast: () => {},\n peerCount,\n lastSyncAt,\n close: () => {},\n }\n }\n\n try {\n channel = new BroadcastChannel(channelName)\n }\n catch (e) {\n console.warn('[useSharedState] Failed to create BroadcastChannel:', e)\n return {\n state,\n broadcast: () => {},\n peerCount,\n lastSyncAt,\n close: () => {},\n }\n }\n\n /** 广播当前状态 */\n function broadcast(): void {\n if (!channel) return\n try {\n channel.postMessage({\n type: 'state-update',\n payload: deepClone(state.value),\n timestamp: Date.now(),\n })\n }\n catch (e) {\n console.warn('[useSharedState] broadcast error:', e)\n }\n }\n\n /** 接收其他标签页的消息 */\n channel.onmessage = (event: MessageEvent) => {\n const msg = event.data\n if (!msg || typeof msg !== 'object') return\n\n if (msg.type === 'state-update') {\n isReceiving = true\n state.value = msg.payload as T\n lastSyncAt.value = msg.timestamp || Date.now()\n isReceiving = false\n }\n else if (msg.type === 'ping') {\n channel?.postMessage({ type: 'pong' })\n }\n else if (msg.type === 'pong') {\n peerCount.value++\n }\n }\n\n /** 监听本地变化并广播 */\n const stopWatch = watch(\n state,\n () => {\n if (!isReceiving) {\n broadcast()\n }\n },\n { deep },\n )\n\n /** 关闭通道 */\n function close(): void {\n stopWatch()\n if (channel) {\n channel.close()\n channel = null\n }\n }\n\n // 通知其他标签页本页存在\n if (immediate) {\n broadcast()\n }\n\n // 发送 ping 探测其他标签页\n if (channel) {\n peerCount.value = 0\n channel.postMessage({ type: 'ping' })\n }\n\n onUnmounted(() => {\n close()\n })\n\n return {\n state,\n broadcast,\n peerCount,\n lastSyncAt,\n close,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAkEO,SAAS,eACd,aACA,cACA,UAAiC,CAAC,GACT;CACzB,MAAM,EAAE,YAAY,OAAO,OAAO,SAAS;CAE3C,MAAM,SAAA,GAAA,IAAA,IAAA,EAAA,GAAA,oBAAA,UAAA,CAAyB,YAAY,CAAC;CAC5C,MAAM,aAAA,GAAA,IAAA,IAAA,CAAgB,CAAC;CACvB,MAAM,cAAA,GAAA,IAAA,IAAA,CAAgC,IAAI;CAE1C,IAAI,UAAmC;CACvC,IAAI,cAAc;CAGlB,IAAI,OAAO,qBAAqB,aAAa;EAC3C,QAAQ,KAAK,qEAAqE;EAClF,OAAO;GACL;GACA,iBAAiB,CAAC;GAClB;GACA;GACA,aAAa,CAAC;EAChB;CACF;CAEA,IAAI;EACF,UAAU,IAAI,iBAAiB,WAAW;CAC5C,SACO,GAAG;EACR,QAAQ,KAAK,uDAAuD,CAAC;EACrE,OAAO;GACL;GACA,iBAAiB,CAAC;GAClB;GACA;GACA,aAAa,CAAC;EAChB;CACF;CAGA,SAAS,YAAkB;EACzB,IAAI,CAAC,SAAS;EACd,IAAI;GACF,QAAQ,YAAY;IAClB,MAAM;IACN,UAAA,GAAA,oBAAA,UAAA,CAAmB,MAAM,KAAK;IAC9B,WAAW,KAAK,IAAI;GACtB,CAAC;EACH,SACO,GAAG;GACR,QAAQ,KAAK,qCAAqC,CAAC;EACrD;CACF;CAGA,QAAQ,aAAa,UAAwB;EAC3C,MAAM,MAAM,MAAM;EAClB,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;EAErC,IAAI,IAAI,SAAS,gBAAgB;GAC/B,cAAc;GACd,MAAM,QAAQ,IAAI;GAClB,WAAW,QAAQ,IAAI,aAAa,KAAK,IAAI;GAC7C,cAAc;EAChB,OAAA,IACS,IAAI,SAAS,QACpB,SAAS,YAAY,EAAE,MAAM,OAAO,CAAC;OACvC,IACS,IAAI,SAAS,QACpB,UAAU;CAEd;CAGA,MAAM,aAAA,GAAA,IAAA,MAAA,CACJ,aACM;EACJ,IAAI,CAAC,aACH,UAAU;CAEd,GACA,EAAE,KAAK,CACT;CAGA,SAAS,QAAc;EACrB,UAAU;EACV,IAAI,SAAS;GACX,QAAQ,MAAM;GACd,UAAU;EACZ;CACF;CAGA,IAAI,WACF,UAAU;CAIZ,IAAI,SAAS;EACX,UAAU,QAAQ;EAClB,QAAQ,YAAY,EAAE,MAAM,OAAO,CAAC;CACtC;CAEA,CAAA,GAAA,IAAA,YAAA,OAAkB;EAChB,MAAM;CACR,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useSharedState.cjs","names":[],"sources":["../../src/composables/useSharedState.ts"],"sourcesContent":["/**\r\n * useSharedState - 跨标签页状态同步\r\n *\r\n * 基于 BroadcastChannel API 实现多标签页实时状态同步\r\n * @module composables/useSharedState\r\n */\r\n\r\nimport type { Ref } from 'vue'\r\nimport { deepClone, deepEqual } from '@composy/store-core'\r\nimport { onUnmounted, ref, watch } from 'vue'\r\n\r\n/**\r\n * useSharedState 选项\r\n */\r\nexport interface UseSharedStateOptions {\r\n /** 是否立即将当前值广播给其他标签页 */\r\n immediate?: boolean\r\n /** 是否深度监听变化 */\r\n deep?: boolean\r\n}\r\n\r\n/**\r\n * useSharedState 返回值\r\n */\r\nexport interface UseSharedStateReturn<T> {\r\n /** 响应式状态 (双向同步) */\r\n state: Ref<T>\r\n /** 手动广播当前状态到其他标签页 */\r\n broadcast: () => void\r\n /** 连接的标签页数量 (近似值) */\r\n peerCount: Ref<number>\r\n /** 最后同步时间 */\r\n lastSyncAt: Ref<number | null>\r\n /** 关闭通道 */\r\n close: () => void\r\n}\r\n\r\n/**\r\n * 跨标签页状态同步 Hook\r\n *\r\n * 使用 BroadcastChannel API 在多个标签页之间实时同步状态。\r\n * 任意标签页修改状态后,其他标签页会自动收到更新。\r\n *\r\n * @template T - 状态类型 (必须可序列化)\r\n * @param channelName - 通道名称 (相同名称的标签页共享状态)\r\n * @param initialState - 初始状态\r\n * @param options - 选项\r\n * @returns 同步状态和控制 API\r\n *\r\n * @example\r\n * ```typescript\r\n * // 标签页 A\r\n * const { state } = useSharedState('my-channel', { count: 0, user: 'Alice' })\r\n * state.value.count++ // 所有标签页都会收到更新\r\n *\r\n * // 标签页 B (相同代码)\r\n * const { state } = useSharedState('my-channel', { count: 0, user: 'Alice' })\r\n * // state.value.count 会自动变为 1\r\n *\r\n * // 手动广播\r\n * broadcast()\r\n *\r\n * // 关闭通道\r\n * close()\r\n * ```\r\n */\r\nexport function useSharedState<T>(\r\n channelName: string,\r\n initialState: T,\r\n options: UseSharedStateOptions = {}\r\n): UseSharedStateReturn<T> {\r\n const { immediate = false, deep = true } = options\r\n\r\n const state = ref<T>(deepClone(initialState)) as Ref<T>\r\n const peerCount = ref(0)\r\n const lastSyncAt = ref<number | null>(null)\r\n\r\n let channel: BroadcastChannel | null = null\r\n const originId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`\r\n let revision = 0\r\n const latestRemoteRevision = new Map<string, number>()\r\n let pendingRemoteState: T\r\n let hasPendingRemoteState = false\r\n\r\n // 检测 BroadcastChannel 是否可用\r\n if (typeof BroadcastChannel === 'undefined') {\r\n console.warn(\r\n '[useSharedState] BroadcastChannel not supported in this environment'\r\n )\r\n return {\r\n state,\r\n broadcast: () => {},\r\n peerCount,\r\n lastSyncAt,\r\n close: () => {},\r\n }\r\n }\r\n\r\n try {\r\n channel = new BroadcastChannel(channelName)\r\n } catch (e) {\r\n console.warn('[useSharedState] Failed to create BroadcastChannel:', e)\r\n return {\r\n state,\r\n broadcast: () => {},\r\n peerCount,\r\n lastSyncAt,\r\n close: () => {},\r\n }\r\n }\r\n\r\n /** 广播当前状态 */\r\n function broadcast(): void {\r\n if (!channel) return\r\n try {\r\n channel.postMessage({\r\n type: 'state-update',\r\n payload: deepClone(state.value),\r\n timestamp: Date.now(),\r\n origin: originId,\r\n revision: ++revision,\r\n })\r\n } catch (e) {\r\n console.warn('[useSharedState] broadcast error:', e)\r\n }\r\n }\r\n\r\n /** 接收其他标签页的消息 */\r\n channel.onmessage = (event: MessageEvent) => {\r\n const msg = event.data\r\n if (!msg || typeof msg !== 'object') return\r\n\r\n if (msg.type === 'state-update') {\r\n if (msg.origin === originId) return\r\n\r\n const remoteRevision = typeof msg.revision === 'number' ? msg.revision : 0\r\n const previousRevision =\r\n latestRemoteRevision.get(String(msg.origin)) ?? -1\r\n if (remoteRevision <= previousRevision) return\r\n latestRemoteRevision.set(String(msg.origin), remoteRevision)\r\n\r\n pendingRemoteState = deepClone(msg.payload as T)\r\n hasPendingRemoteState = true\r\n state.value = pendingRemoteState\r\n lastSyncAt.value = msg.timestamp || Date.now()\r\n } else if (msg.type === 'ping') {\r\n channel?.postMessage({ type: 'pong' })\r\n } else if (msg.type === 'pong') {\r\n peerCount.value++\r\n }\r\n }\r\n\r\n /** 监听本地变化并广播 */\r\n const stopWatch = watch(\r\n state,\r\n () => {\r\n if (hasPendingRemoteState) {\r\n if (deepEqual(state.value, pendingRemoteState)) {\r\n hasPendingRemoteState = false\r\n return\r\n }\r\n hasPendingRemoteState = false\r\n }\r\n broadcast()\r\n },\r\n { deep }\r\n )\r\n\r\n /** 关闭通道 */\r\n function close(): void {\r\n stopWatch()\r\n if (channel) {\r\n channel.close()\r\n channel = null\r\n }\r\n }\r\n\r\n // 通知其他标签页本页存在\r\n if (immediate) {\r\n broadcast()\r\n }\r\n\r\n // 发送 ping 探测其他标签页\r\n if (channel) {\r\n peerCount.value = 0\r\n channel.postMessage({ type: 'ping' })\r\n }\r\n\r\n onUnmounted(() => {\r\n close()\r\n })\r\n\r\n return {\r\n state,\r\n broadcast,\r\n peerCount,\r\n lastSyncAt,\r\n close,\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;AAkEO,SAAS,eACd,aACA,cACA,UAAiC,CAAC,GACT;CACzB,MAAM,EAAE,YAAY,OAAO,OAAO,SAAS;CAE3C,MAAM,SAAA,GAAA,IAAA,IAAA,EAAA,GAAA,oBAAA,UAAA,CAAyB,YAAY,CAAC;CAC5C,MAAM,aAAA,GAAA,IAAA,IAAA,CAAgB,CAAC;CACvB,MAAM,cAAA,GAAA,IAAA,IAAA,CAAgC,IAAI;CAE1C,IAAI,UAAmC;CACvC,MAAM,WAAW,GAAG,KAAK,IAAI,EAAC,GAAI,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,EAAE;CACxE,IAAI,WAAW;CACf,MAAM,uCAAuB,IAAI,IAAoB;CACrD,IAAI;CACJ,IAAI,wBAAwB;CAG5B,IAAI,OAAO,qBAAqB,aAAa;EAC3C,QAAQ,KACN,qEACF;EACA,OAAO;GACL;GACA,iBAAiB,CAAC;GAClB;GACA;GACA,aAAa,CAAC;EAChB;CACF;CAEA,IAAI;EACF,UAAU,IAAI,iBAAiB,WAAW;CAC5C,SAAS,GAAG;EACV,QAAQ,KAAK,uDAAuD,CAAC;EACrE,OAAO;GACL;GACA,iBAAiB,CAAC;GAClB;GACA;GACA,aAAa,CAAC;EAChB;CACF;CAGA,SAAS,YAAkB;EACzB,IAAI,CAAC,SAAS;EACd,IAAI;GACF,QAAQ,YAAY;IAClB,MAAM;IACN,UAAA,GAAA,oBAAA,UAAA,CAAmB,MAAM,KAAK;IAC9B,WAAW,KAAK,IAAI;IACpB,QAAQ;IACR,UAAU,EAAE;GACd,CAAC;EACH,SAAS,GAAG;GACV,QAAQ,KAAK,qCAAqC,CAAC;EACrD;CACF;CAGA,QAAQ,aAAa,UAAwB;EAC3C,MAAM,MAAM,MAAM;EAClB,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;EAErC,IAAI,IAAI,SAAS,gBAAgB;GAC/B,IAAI,IAAI,WAAW,UAAU;GAE7B,MAAM,iBAAiB,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW;GAGzE,IAAI,mBADF,qBAAqB,IAAI,OAAO,IAAI,MAAM,CAAC,KAAK,KACV;GACxC,qBAAqB,IAAI,OAAO,IAAI,MAAM,GAAG,cAAc;GAE3D,sBAAA,GAAA,oBAAA,UAAA,CAA+B,IAAI,OAAY;GAC/C,wBAAwB;GACxB,MAAM,QAAQ;GACd,WAAW,QAAQ,IAAI,aAAa,KAAK,IAAI;EAC/C,OAAA,IAAW,IAAI,SAAS,QACtB,SAAS,YAAY,EAAE,MAAM,OAAO,CAAC;OACvC,IAAW,IAAI,SAAS,QACtB,UAAU;CAEd;CAGA,MAAM,aAAA,GAAA,IAAA,MAAA,CACJ,aACM;EACJ,IAAI,uBAAuB;GACzB,KAAA,GAAA,oBAAA,UAAA,CAAc,MAAM,OAAO,kBAAkB,GAAG;IAC9C,wBAAwB;IACxB;GACF;GACA,wBAAwB;EAC1B;EACA,UAAU;CACZ,GACA,EAAE,KAAK,CACT;CAGA,SAAS,QAAc;EACrB,UAAU;EACV,IAAI,SAAS;GACX,QAAQ,MAAM;GACd,UAAU;EACZ;CACF;CAGA,IAAI,WACF,UAAU;CAIZ,IAAI,SAAS;EACX,UAAU,QAAQ;EAClB,QAAQ,YAAY,EAAE,MAAM,OAAO,CAAC;CACtC;CAEA,CAAA,GAAA,IAAA,YAAA,OAAkB;EAChB,MAAM;CACR,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -59,4 +59,3 @@ export interface UseSharedStateReturn<T> {
59
59
  * ```
60
60
  */
61
61
  export declare function useSharedState<T>(channelName: string, initialState: T, options?: UseSharedStateOptions): UseSharedStateReturn<T>;
62
- //# sourceMappingURL=useSharedState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSharedState.d.cts","sourceRoot":"","sources":["../../src/composables/useSharedState.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAI9B;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,eAAe;IACf,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,mBAAmB;IACnB,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IACb,qBAAqB;IACrB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,qBAAqB;IACrB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACtB,aAAa;IACb,UAAU,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC9B,WAAW;IACX,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAC9B,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAAC,CAAC,CAAC,CAiIzB"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/store-vue v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:39:29 *
5
+ * Build time: 2024-10-12 17:40:04 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -1 +1 @@
1
- {"version":3,"file":"useStore.cjs","names":[],"sources":["../../src/composables/useStore.ts"],"sourcesContent":["/**\n * useStore Composable\n *\n * 提供在组件中使用 Store 的便捷方法\n * @module composables/useStore\n */\n\nimport type { StoreGeneric } from 'pinia'\nimport type { Ref } from 'vue'\nimport { storeToRefs } from 'pinia'\nimport { computed, ref, watch } from 'vue'\n\n/**\n * useStore 选项\n */\nexport interface UseStoreOptions {\n /** 是否立即同步状态 */\n immediate?: boolean\n /** 是否深度监听 */\n deep?: boolean\n}\n\n/**\n * 通用 Store Hook\n *\n * 提供对 Pinia Store 的便捷访问\n *\n * @template T - Store 类型\n * @param useStoreHook - Pinia useStore hook\n * @param _options - 选项\n * @returns Store 包装对象\n *\n * @example\n * ```typescript\n * // 定义 Store\n * const useCounterStore = defineStore('counter', {\n * state: () => ({ count: 0 }),\n * actions: {\n * increment() { this.count++ }\n * }\n * })\n *\n * // 在组件中使用\n * const { store, refs, reset } = useStore(useCounterStore)\n *\n * // 访问响应式状态\n * console.log(refs.count.value)\n *\n * // 调用 action\n * store.increment()\n * ```\n */\nexport function useStore<T extends StoreGeneric>(\n useStoreHook: () => T,\n _options: UseStoreOptions = {},\n): {\n store: T\n refs: ReturnType<typeof storeToRefs<T>>\n reset: () => void\n patch: (partial: Partial<T> | ((state: T) => void)) => void\n} {\n const store = useStoreHook()\n\n // 获取响应式 refs\n const refs = storeToRefs(store)\n\n // 重置函数\n const reset = (): void => {\n if ('$reset' in store && typeof store.$reset === 'function') {\n (store.$reset as () => void)()\n }\n }\n\n // 批量更新函数\n const patch = (partial: Partial<T> | ((state: T) => void)): void => {\n if ('$patch' in store && typeof store.$patch === 'function') {\n (store.$patch as (partial: unknown) => void)(partial)\n }\n }\n\n return {\n store,\n refs,\n reset,\n patch,\n }\n}\n\n/**\n * 创建简单的响应式 Store\n *\n * 不依赖 Pinia,适用于简单场景\n *\n * @template S - 状态类型\n * @param initialState - 初始状态\n * @returns 响应式 Store\n *\n * @example\n * ```typescript\n * const { state, setState, reset } = useSimpleStore({\n * count: 0,\n * name: 'test'\n * })\n *\n * // 更新状态\n * setState({ count: 1 })\n *\n * // 重置\n * reset()\n * ```\n */\nexport function useSimpleStore<S extends Record<string, unknown>>(\n initialState: S,\n): {\n state: Ref<S>\n setState: (partial: Partial<S>) => void\n reset: () => void\n subscribe: (callback: (state: S) => void) => () => void\n} {\n const state = ref({ ...initialState }) as Ref<S>\n const initialStateCopy = { ...initialState }\n\n const setState = (partial: Partial<S>): void => {\n Object.assign(state.value, partial)\n }\n\n const reset = (): void => {\n state.value = { ...initialStateCopy } as S\n }\n\n const subscribe = (callback: (state: S) => void): (() => void) => {\n const stop = watch(\n state,\n (newState) => {\n callback(newState)\n },\n { deep: true },\n )\n return stop\n }\n\n return {\n state,\n setState,\n reset,\n subscribe,\n }\n}\n\n/**\n * 创建计算属性 Store\n *\n * @template S - 状态类型\n * @template G - Getters 类型\n * @param state - 状态\n * @param getters - 计算属性\n * @returns 带计算属性的 Store\n */\nexport function useComputedStore<\n S extends Record<string, unknown>,\n G extends Record<string, (state: S) => unknown>,\n>(\n state: Ref<S>,\n getters: G,\n): {\n state: Ref<S>\n getters: { [K in keyof G]: ReturnType<G[K]> }\n} {\n const computedGetters = {} as { [K in keyof G]: ReturnType<G[K]> }\n\n for (const key in getters) {\n (computedGetters as Record<string, unknown>)[key] = computed(() =>\n getters[key](state.value),\n )\n }\n\n return {\n state,\n getters: computedGetters,\n }\n}\n\n"],"mappings":";;;;;;;;;;;;;;AAoDO,SAAS,SACd,cACA,WAA4B,CAAC,GAM7B;CACA,MAAM,QAAQ,aAAa;CAG3B,MAAM,QAAA,GAAA,MAAA,YAAA,CAAmB,KAAK;CAG9B,MAAM,cAAoB;EACxB,IAAI,YAAY,SAAS,OAAO,MAAM,WAAW,YAC9C,MAAM,OAAsB;CAEjC;CAGA,MAAM,SAAS,YAAqD;EAClE,IAAI,YAAY,SAAS,OAAO,MAAM,WAAW,YAC9C,MAAM,OAAsC,OAAO;CAExD;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF;AAyBO,SAAS,eACd,cAMA;CACA,MAAM,SAAA,GAAA,IAAA,IAAA,CAAY,EAAE,GAAG,aAAa,CAAC;CACrC,MAAM,mBAAmB,EAAE,GAAG,aAAa;CAE3C,MAAM,YAAY,YAA8B;EAC9C,OAAO,OAAO,MAAM,OAAO,OAAO;CACpC;CAEA,MAAM,cAAoB;EACxB,MAAM,QAAQ,EAAE,GAAG,iBAAiB;CACtC;CAEA,MAAM,aAAa,aAA+C;EAQhE,QAAA,GAAA,IAAA,MAAA,CANE,QACC,aAAa;GACZ,SAAS,QAAQ;EACnB,GACA,EAAE,MAAM,KAAK,CAER;CACT;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF;AAWO,SAAS,iBAId,OACA,SAIA;CACA,MAAM,kBAAkB,CAAC;CAEzB,KAAA,MAAW,OAAO,SACf,gBAA4C,QAAA,GAAA,IAAA,SAAA,OAC3C,QAAQ,IAAG,CAAE,MAAM,KAAK,CAC1B;CAGF,OAAO;EACL;EACA,SAAS;CACX;AACF"}
1
+ {"version":3,"file":"useStore.cjs","names":[],"sources":["../../src/composables/useStore.ts"],"sourcesContent":["/**\r\n * useStore Composable\r\n *\r\n * 提供在组件中使用 Store 的便捷方法\r\n * @module composables/useStore\r\n */\r\n\r\nimport type { StoreGeneric } from 'pinia'\r\nimport type { Ref } from 'vue'\r\nimport { storeToRefs } from 'pinia'\r\nimport { computed, ref, watch } from 'vue'\r\n\r\n/**\r\n * useStore 选项\r\n */\r\nexport interface UseStoreOptions {\r\n /** 是否立即同步状态 */\r\n immediate?: boolean\r\n /** 是否深度监听 */\r\n deep?: boolean\r\n}\r\n\r\n/**\r\n * 通用 Store Hook\r\n *\r\n * 提供对 Pinia Store 的便捷访问\r\n *\r\n * @template T - Store 类型\r\n * @param useStoreHook - Pinia useStore hook\r\n * @param _options - 选项\r\n * @returns Store 包装对象\r\n *\r\n * @example\r\n * ```typescript\r\n * // 定义 Store\r\n * const useCounterStore = defineStore('counter', {\r\n * state: () => ({ count: 0 }),\r\n * actions: {\r\n * increment() { this.count++ }\r\n * }\r\n * })\r\n *\r\n * // 在组件中使用\r\n * const { store, refs, reset } = useStore(useCounterStore)\r\n *\r\n * // 访问响应式状态\r\n * console.log(refs.count.value)\r\n *\r\n * // 调用 action\r\n * store.increment()\r\n * ```\r\n */\r\nexport function useStore<T extends StoreGeneric>(\r\n useStoreHook: () => T,\r\n _options: UseStoreOptions = {}\r\n): {\r\n store: T\r\n refs: ReturnType<typeof storeToRefs<T>>\r\n reset: () => void\r\n patch: (partial: Partial<T> | ((state: T) => void)) => void\r\n} {\r\n const store = useStoreHook()\r\n\r\n // 获取响应式 refs\r\n const refs = storeToRefs(store)\r\n\r\n // 重置函数\r\n const reset = (): void => {\r\n if ('$reset' in store && typeof store.$reset === 'function') {\r\n ;(store.$reset as () => void)()\r\n }\r\n }\r\n\r\n // 批量更新函数\r\n const patch = (partial: Partial<T> | ((state: T) => void)): void => {\r\n if ('$patch' in store && typeof store.$patch === 'function') {\r\n ;(store.$patch as (partial: unknown) => void)(partial)\r\n }\r\n }\r\n\r\n return {\r\n store,\r\n refs,\r\n reset,\r\n patch,\r\n }\r\n}\r\n\r\n/**\r\n * 创建简单的响应式 Store\r\n *\r\n * 不依赖 Pinia,适用于简单场景\r\n *\r\n * @template S - 状态类型\r\n * @param initialState - 初始状态\r\n * @returns 响应式 Store\r\n *\r\n * @example\r\n * ```typescript\r\n * const { state, setState, reset } = useSimpleStore({\r\n * count: 0,\r\n * name: 'test'\r\n * })\r\n *\r\n * // 更新状态\r\n * setState({ count: 1 })\r\n *\r\n * // 重置\r\n * reset()\r\n * ```\r\n */\r\nexport function useSimpleStore<S extends Record<string, unknown>>(\r\n initialState: S\r\n): {\r\n state: Ref<S>\r\n setState: (partial: Partial<S>) => void\r\n reset: () => void\r\n subscribe: (callback: (state: S) => void) => () => void\r\n} {\r\n const state = ref({ ...initialState }) as Ref<S>\r\n const initialStateCopy = { ...initialState }\r\n\r\n const setState = (partial: Partial<S>): void => {\r\n Object.assign(state.value, partial)\r\n }\r\n\r\n const reset = (): void => {\r\n state.value = { ...initialStateCopy } as S\r\n }\r\n\r\n const subscribe = (callback: (state: S) => void): (() => void) => {\r\n const stop = watch(\r\n state,\r\n newState => {\r\n callback(newState)\r\n },\r\n { deep: true }\r\n )\r\n return stop\r\n }\r\n\r\n return {\r\n state,\r\n setState,\r\n reset,\r\n subscribe,\r\n }\r\n}\r\n\r\n/**\r\n * 创建计算属性 Store\r\n *\r\n * @template S - 状态类型\r\n * @template G - Getters 类型\r\n * @param state - 状态\r\n * @param getters - 计算属性\r\n * @returns 带计算属性的 Store\r\n */\r\nexport function useComputedStore<\r\n S extends Record<string, unknown>,\r\n G extends Record<string, (state: S) => unknown>,\r\n>(\r\n state: Ref<S>,\r\n getters: G\r\n): {\r\n state: Ref<S>\r\n getters: { [K in keyof G]: ReturnType<G[K]> }\r\n} {\r\n const computedGetters = {} as { [K in keyof G]: ReturnType<G[K]> }\r\n\r\n for (const key in getters) {\r\n ;(computedGetters as Record<string, unknown>)[key] = computed(() =>\r\n getters[key](state.value)\r\n )\r\n }\r\n\r\n return {\r\n state,\r\n getters: computedGetters,\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;AAoDO,SAAS,SACd,cACA,WAA4B,CAAC,GAM7B;CACA,MAAM,QAAQ,aAAa;CAG3B,MAAM,QAAA,GAAA,MAAA,YAAA,CAAmB,KAAK;CAG9B,MAAM,cAAoB;EACxB,IAAI,YAAY,SAAS,OAAO,MAAM,WAAW,YAC7C,MAAM,OAAsB;CAElC;CAGA,MAAM,SAAS,YAAqD;EAClE,IAAI,YAAY,SAAS,OAAO,MAAM,WAAW,YAC7C,MAAM,OAAsC,OAAO;CAEzD;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF;AAyBO,SAAS,eACd,cAMA;CACA,MAAM,SAAA,GAAA,IAAA,IAAA,CAAY,EAAE,GAAG,aAAa,CAAC;CACrC,MAAM,mBAAmB,EAAE,GAAG,aAAa;CAE3C,MAAM,YAAY,YAA8B;EAC9C,OAAO,OAAO,MAAM,OAAO,OAAO;CACpC;CAEA,MAAM,cAAoB;EACxB,MAAM,QAAQ,EAAE,GAAG,iBAAiB;CACtC;CAEA,MAAM,aAAa,aAA+C;EAQhE,QAAA,GAAA,IAAA,MAAA,CANE,QACA,aAAY;GACV,SAAS,QAAQ;EACnB,GACA,EAAE,MAAM,KAAK,CAER;CACT;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF;AAWO,SAAS,iBAId,OACA,SAIA;CACA,MAAM,kBAAkB,CAAC;CAEzB,KAAA,MAAW,OAAO,SACd,gBAA4C,QAAA,GAAA,IAAA,SAAA,OAC5C,QAAQ,IAAG,CAAE,MAAM,KAAK,CAC1B;CAGF,OAAO;EACL;EACA,SAAS;CACX;AACF"}
@@ -96,4 +96,3 @@ export declare function useComputedStore<S extends Record<string, unknown>, G ex
96
96
  [K in keyof G]: ReturnType<G[K]>;
97
97
  };
98
98
  };
99
- //# sourceMappingURL=useStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStore.d.cts","sourceRoot":"","sources":["../../src/composables/useStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnC;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,aAAa;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,YAAY,EAC7C,YAAY,EAAE,MAAM,CAAC,EACrB,QAAQ,GAAE,eAAoB,GAC7B;IACD,KAAK,EAAE,CAAC,CAAA;IACR,IAAI,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IACvC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,IAAI,CAAA;CAC5D,CA0BA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,YAAY,EAAE,CAAC,GACd;IACD,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IACb,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACvC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;CACxD,CA6BA;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,EAE/C,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EACb,OAAO,EAAE,CAAC,GACT;IACD,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IACb,OAAO,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAA;CAC9C,CAaA"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/store-vue v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:39:29 *
5
+ * Build time: 2024-10-12 17:40:04 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -1 +1 @@
1
- {"version":3,"file":"useStoreDevtools.cjs","names":[],"sources":["../../src/composables/useStoreDevtools.ts"],"sourcesContent":["import type { EventData } from '@composy/store-core'\nimport type { Pinia, StoreGeneric } from 'pinia'\nimport type { Ref } from 'vue'\nimport { deepClone, globalEventBus } from '@composy/store-core'\nimport { getActivePinia } from 'pinia'\nimport { onUnmounted, ref } from 'vue'\n\nconst STORE_REFRESH_EVENTS = new Set([\n 'store:created',\n 'store:disposed',\n 'state:changed',\n])\n\ntype PiniaWithStores = Pinia & {\n _s?: Map<string, StoreGeneric>\n}\n\nexport interface StoreInfo {\n id: string\n state: Record<string, unknown>\n active: boolean\n}\n\nexport interface EventLogEntry {\n type: string\n payload: unknown\n timestamp: number\n time: string\n}\n\nexport interface UseStoreDevtoolsReturn {\n stores: Ref<StoreInfo[]>\n eventLog: Ref<EventLogEntry[]>\n isMonitoring: Ref<boolean>\n startMonitoring: () => void\n stopMonitoring: () => void\n clearLog: () => void\n refreshStores: () => void\n getStoreState: (id: string) => Record<string, unknown> | null\n resetStore: (id: string) => boolean\n resetAllStores: () => void\n}\n\nfunction formatTime(timestamp: number): string {\n const date = new Date(timestamp)\n const hours = date.getHours().toString().padStart(2, '0')\n const minutes = date.getMinutes().toString().padStart(2, '0')\n const seconds = date.getSeconds().toString().padStart(2, '0')\n const milliseconds = date.getMilliseconds().toString().padStart(3, '0')\n\n return `${hours}:${minutes}:${seconds}.${milliseconds}`\n}\n\nfunction getPinia(): PiniaWithStores | null {\n return (getActivePinia() ?? null) as PiniaWithStores | null\n}\n\nfunction snapshotStore(store: StoreGeneric, id: string): StoreInfo {\n return {\n id,\n state: deepClone(store.$state),\n active: true,\n }\n}\n\nexport function useStoreDevtools(options: {\n /**\n * 最多保留多少条事件日志。\n */\n maxLogEntries?: number\n} = {}): UseStoreDevtoolsReturn {\n const { maxLogEntries = 200 } = options\n\n const stores = ref<StoreInfo[]>([])\n const eventLog = ref<EventLogEntry[]>([])\n const isMonitoring = ref(false)\n\n let unsubscribe: (() => void) | null = null\n\n function refreshStores(): void {\n const pinia = getPinia()\n if (!pinia?._s) {\n stores.value = []\n return\n }\n\n stores.value = Array.from(pinia._s.entries(), ([id, store]) => {\n return snapshotStore(store, id)\n })\n }\n\n function startMonitoring(): void {\n if (isMonitoring.value) {\n return\n }\n\n isMonitoring.value = true\n unsubscribe = globalEventBus.on('*', (data: EventData) => {\n const entry: EventLogEntry = {\n type: data.type,\n payload: data.payload,\n timestamp: data.timestamp,\n time: formatTime(data.timestamp),\n }\n\n eventLog.value = [entry, ...eventLog.value].slice(0, maxLogEntries)\n\n if (STORE_REFRESH_EVENTS.has(data.type)) {\n refreshStores()\n }\n })\n }\n\n function stopMonitoring(): void {\n if (!isMonitoring.value) {\n return\n }\n\n isMonitoring.value = false\n unsubscribe?.()\n unsubscribe = null\n }\n\n function clearLog(): void {\n eventLog.value = []\n }\n\n function getStoreState(id: string): Record<string, unknown> | null {\n const pinia = getPinia()\n const store = pinia?._s?.get(id)\n\n if (!store) {\n return null\n }\n\n return deepClone(store.$state)\n }\n\n function resetStore(id: string): boolean {\n const pinia = getPinia()\n const store = pinia?._s?.get(id)\n\n if (!store || typeof store.$reset !== 'function') {\n return false\n }\n\n store.$reset()\n refreshStores()\n return true\n }\n\n function resetAllStores(): void {\n const pinia = getPinia()\n if (!pinia?._s) {\n return\n }\n\n pinia._s.forEach((store) => {\n if (typeof store.$reset === 'function') {\n store.$reset()\n }\n })\n\n refreshStores()\n }\n\n refreshStores()\n\n onUnmounted(() => {\n stopMonitoring()\n })\n\n return {\n stores,\n eventLog,\n isMonitoring,\n startMonitoring,\n stopMonitoring,\n clearLog,\n refreshStores,\n getStoreState,\n resetStore,\n resetAllStores,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAOA,MAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;AACF,CAAC;AAgCD,SAAS,WAAW,WAA2B;CAC7C,MAAM,OAAO,IAAI,KAAK,SAAS;CAM/B,OAAO,GALO,KAAK,SAAS,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAK3C,EAAK,GAJC,KAAK,WAAW,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAItC,EAAO,GAHV,KAAK,WAAW,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAG3B,EAAO,GAFhB,KAAK,gBAAgB,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAE1B;AAC3C;AAEA,SAAS,WAAmC;CAC1C,QAAA,GAAA,MAAA,eAAA,CAAuB,KAAK;AAC9B;AAEA,SAAS,cAAc,OAAqB,IAAuB;CACjE,OAAO;EACL;EACA,QAAA,GAAA,oBAAA,UAAA,CAAiB,MAAM,MAAM;EAC7B,QAAQ;CACV;AACF;AAEO,SAAS,iBAAiB,UAK7B,CAAC,GAA2B;CAC9B,MAAM,EAAE,gBAAgB,QAAQ;CAEhC,MAAM,UAAA,GAAA,IAAA,IAAA,CAA0B,CAAC,CAAC;CAClC,MAAM,YAAA,GAAA,IAAA,IAAA,CAAgC,CAAC,CAAC;CACxC,MAAM,gBAAA,GAAA,IAAA,IAAA,CAAmB,KAAK;CAE9B,IAAI,cAAmC;CAEvC,SAAS,gBAAsB;EAC7B,MAAM,UAAQ,SAAS;EACvB,IAAI,CAAC,SAAO,IAAI;GACd,OAAO,QAAQ,CAAC;GAChB;EACF;EAEA,OAAO,QAAQ,MAAM,KAAK,QAAM,GAAG,QAAQ,IAAI,CAAC,IAAI,WAAW;GAC7D,OAAO,cAAc,OAAO,EAAE;EAChC,CAAC;CACH;CAEA,SAAS,kBAAwB;EAC/B,IAAI,aAAa,OACf;EAGF,aAAa,QAAQ;EACrB,cAAc,oBAAA,eAAe,GAAG,MAAM,SAAoB;GACxD,MAAM,QAAuB;IAC3B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,WAAW,KAAK;IAChB,MAAM,WAAW,KAAK,SAAS;GACjC;GAEA,SAAS,QAAQ,CAAC,OAAO,GAAG,SAAS,KAAK,CAAA,CAAE,MAAM,GAAG,aAAa;GAElE,IAAI,qBAAqB,IAAI,KAAK,IAAI,GACpC,cAAc;EAElB,CAAC;CACH;CAEA,SAAS,iBAAuB;EAC9B,IAAI,CAAC,aAAa,OAChB;EAGF,aAAa,QAAQ;EACrB,cAAc;EACd,cAAc;CAChB;CAEA,SAAS,WAAiB;EACxB,SAAS,QAAQ,CAAC;CACpB;CAEA,SAAS,cAAc,IAA4C;EAEjE,MAAM,QADQ,SACA,CAAA,EAAO,IAAI,IAAI,EAAE;EAE/B,IAAI,CAAC,OACH,OAAO;EAGT,QAAA,GAAA,oBAAA,UAAA,CAAiB,MAAM,MAAM;CAC/B;CAEA,SAAS,WAAW,IAAqB;EAEvC,MAAM,QADQ,SACA,CAAA,EAAO,IAAI,IAAI,EAAE;EAE/B,IAAI,CAAC,SAAS,OAAO,MAAM,WAAW,YACpC,OAAO;EAGT,MAAM,OAAO;EACb,cAAc;EACd,OAAO;CACT;CAEA,SAAS,iBAAuB;EAC9B,MAAM,UAAQ,SAAS;EACvB,IAAI,CAAC,SAAO,IACV;EAGF,QAAM,GAAG,SAAS,UAAU;GAC1B,IAAI,OAAO,MAAM,WAAW,YAC1B,MAAM,OAAO;EAEjB,CAAC;EAED,cAAc;CAChB;CAEA,cAAc;CAEd,CAAA,GAAA,IAAA,YAAA,OAAkB;EAChB,eAAe;CACjB,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useStoreDevtools.cjs","names":[],"sources":["../../src/composables/useStoreDevtools.ts"],"sourcesContent":["import type { EventData } from '@composy/store-core'\r\nimport type { Pinia, StoreGeneric } from 'pinia'\r\nimport type { Ref } from 'vue'\r\nimport { deepClone, globalEventBus } from '@composy/store-core'\r\nimport { getActivePinia } from 'pinia'\r\nimport { onUnmounted, ref } from 'vue'\r\n\r\nconst STORE_REFRESH_EVENTS = new Set([\r\n 'store:created',\r\n 'store:disposed',\r\n 'state:changed',\r\n])\r\n\r\ntype PiniaWithStores = Pinia & {\r\n _s?: Map<string, StoreGeneric>\r\n}\r\n\r\nexport interface StoreInfo {\r\n id: string\r\n state: Record<string, unknown>\r\n active: boolean\r\n}\r\n\r\nexport interface EventLogEntry {\r\n type: string\r\n payload: unknown\r\n timestamp: number\r\n time: string\r\n}\r\n\r\nexport interface UseStoreDevtoolsReturn {\r\n stores: Ref<StoreInfo[]>\r\n eventLog: Ref<EventLogEntry[]>\r\n isMonitoring: Ref<boolean>\r\n startMonitoring: () => void\r\n stopMonitoring: () => void\r\n clearLog: () => void\r\n refreshStores: () => void\r\n getStoreState: (id: string) => Record<string, unknown> | null\r\n resetStore: (id: string) => boolean\r\n resetAllStores: () => void\r\n}\r\n\r\nfunction formatTime(timestamp: number): string {\r\n const date = new Date(timestamp)\r\n const hours = date.getHours().toString().padStart(2, '0')\r\n const minutes = date.getMinutes().toString().padStart(2, '0')\r\n const seconds = date.getSeconds().toString().padStart(2, '0')\r\n const milliseconds = date.getMilliseconds().toString().padStart(3, '0')\r\n\r\n return `${hours}:${minutes}:${seconds}.${milliseconds}`\r\n}\r\n\r\nfunction getPinia(): PiniaWithStores | null {\r\n return (getActivePinia() ?? null) as PiniaWithStores | null\r\n}\r\n\r\nfunction snapshotStore(store: StoreGeneric, id: string): StoreInfo {\r\n return {\r\n id,\r\n state: deepClone(store.$state),\r\n active: true,\r\n }\r\n}\r\n\r\nexport function useStoreDevtools(\r\n options: {\r\n /**\r\n * 最多保留多少条事件日志。\r\n */\r\n maxLogEntries?: number\r\n } = {}\r\n): UseStoreDevtoolsReturn {\r\n const { maxLogEntries = 200 } = options\r\n\r\n const stores = ref<StoreInfo[]>([])\r\n const eventLog = ref<EventLogEntry[]>([])\r\n const isMonitoring = ref(false)\r\n\r\n let unsubscribe: (() => void) | null = null\r\n\r\n function refreshStores(): void {\r\n const pinia = getPinia()\r\n if (!pinia?._s) {\r\n stores.value = []\r\n return\r\n }\r\n\r\n stores.value = Array.from(pinia._s.entries(), ([id, store]) => {\r\n return snapshotStore(store, id)\r\n })\r\n }\r\n\r\n function startMonitoring(): void {\r\n if (isMonitoring.value) {\r\n return\r\n }\r\n\r\n isMonitoring.value = true\r\n unsubscribe = globalEventBus.on('*', (data: EventData) => {\r\n const entry: EventLogEntry = {\r\n type: data.type,\r\n payload: data.payload,\r\n timestamp: data.timestamp,\r\n time: formatTime(data.timestamp),\r\n }\r\n\r\n eventLog.value = [entry, ...eventLog.value].slice(0, maxLogEntries)\r\n\r\n if (STORE_REFRESH_EVENTS.has(data.type)) {\r\n refreshStores()\r\n }\r\n })\r\n }\r\n\r\n function stopMonitoring(): void {\r\n if (!isMonitoring.value) {\r\n return\r\n }\r\n\r\n isMonitoring.value = false\r\n unsubscribe?.()\r\n unsubscribe = null\r\n }\r\n\r\n function clearLog(): void {\r\n eventLog.value = []\r\n }\r\n\r\n function getStoreState(id: string): Record<string, unknown> | null {\r\n const pinia = getPinia()\r\n const store = pinia?._s?.get(id)\r\n\r\n if (!store) {\r\n return null\r\n }\r\n\r\n return deepClone(store.$state)\r\n }\r\n\r\n function resetStore(id: string): boolean {\r\n const pinia = getPinia()\r\n const store = pinia?._s?.get(id)\r\n\r\n if (!store || typeof store.$reset !== 'function') {\r\n return false\r\n }\r\n\r\n store.$reset()\r\n refreshStores()\r\n return true\r\n }\r\n\r\n function resetAllStores(): void {\r\n const pinia = getPinia()\r\n if (!pinia?._s) {\r\n return\r\n }\r\n\r\n pinia._s.forEach(store => {\r\n if (typeof store.$reset === 'function') {\r\n store.$reset()\r\n }\r\n })\r\n\r\n refreshStores()\r\n }\r\n\r\n refreshStores()\r\n\r\n onUnmounted(() => {\r\n stopMonitoring()\r\n })\r\n\r\n return {\r\n stores,\r\n eventLog,\r\n isMonitoring,\r\n startMonitoring,\r\n stopMonitoring,\r\n clearLog,\r\n refreshStores,\r\n getStoreState,\r\n resetStore,\r\n resetAllStores,\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;AAOA,MAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;AACF,CAAC;AAgCD,SAAS,WAAW,WAA2B;CAC7C,MAAM,OAAO,IAAI,KAAK,SAAS;CAM/B,OAAO,GALO,KAAK,SAAS,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAK3C,EAAK,GAJC,KAAK,WAAW,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAItC,EAAO,GAHV,KAAK,WAAW,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAG3B,EAAO,GAFhB,KAAK,gBAAgB,CAAA,CAAE,SAAS,CAAA,CAAE,SAAS,GAAG,GAE1B;AAC3C;AAEA,SAAS,WAAmC;CAC1C,QAAA,GAAA,MAAA,eAAA,CAAuB,KAAK;AAC9B;AAEA,SAAS,cAAc,OAAqB,IAAuB;CACjE,OAAO;EACL;EACA,QAAA,GAAA,oBAAA,UAAA,CAAiB,MAAM,MAAM;EAC7B,QAAQ;CACV;AACF;AAEO,SAAS,iBACd,UAKI,CAAC,GACmB;CACxB,MAAM,EAAE,gBAAgB,QAAQ;CAEhC,MAAM,UAAA,GAAA,IAAA,IAAA,CAA0B,CAAC,CAAC;CAClC,MAAM,YAAA,GAAA,IAAA,IAAA,CAAgC,CAAC,CAAC;CACxC,MAAM,gBAAA,GAAA,IAAA,IAAA,CAAmB,KAAK;CAE9B,IAAI,cAAmC;CAEvC,SAAS,gBAAsB;EAC7B,MAAM,UAAQ,SAAS;EACvB,IAAI,CAAC,SAAO,IAAI;GACd,OAAO,QAAQ,CAAC;GAChB;EACF;EAEA,OAAO,QAAQ,MAAM,KAAK,QAAM,GAAG,QAAQ,IAAI,CAAC,IAAI,WAAW;GAC7D,OAAO,cAAc,OAAO,EAAE;EAChC,CAAC;CACH;CAEA,SAAS,kBAAwB;EAC/B,IAAI,aAAa,OACf;EAGF,aAAa,QAAQ;EACrB,cAAc,oBAAA,eAAe,GAAG,MAAM,SAAoB;GACxD,MAAM,QAAuB;IAC3B,MAAM,KAAK;IACX,SAAS,KAAK;IACd,WAAW,KAAK;IAChB,MAAM,WAAW,KAAK,SAAS;GACjC;GAEA,SAAS,QAAQ,CAAC,OAAO,GAAG,SAAS,KAAK,CAAA,CAAE,MAAM,GAAG,aAAa;GAElE,IAAI,qBAAqB,IAAI,KAAK,IAAI,GACpC,cAAc;EAElB,CAAC;CACH;CAEA,SAAS,iBAAuB;EAC9B,IAAI,CAAC,aAAa,OAChB;EAGF,aAAa,QAAQ;EACrB,cAAc;EACd,cAAc;CAChB;CAEA,SAAS,WAAiB;EACxB,SAAS,QAAQ,CAAC;CACpB;CAEA,SAAS,cAAc,IAA4C;EAEjE,MAAM,QADQ,SACA,CAAA,EAAO,IAAI,IAAI,EAAE;EAE/B,IAAI,CAAC,OACH,OAAO;EAGT,QAAA,GAAA,oBAAA,UAAA,CAAiB,MAAM,MAAM;CAC/B;CAEA,SAAS,WAAW,IAAqB;EAEvC,MAAM,QADQ,SACA,CAAA,EAAO,IAAI,IAAI,EAAE;EAE/B,IAAI,CAAC,SAAS,OAAO,MAAM,WAAW,YACpC,OAAO;EAGT,MAAM,OAAO;EACb,cAAc;EACd,OAAO;CACT;CAEA,SAAS,iBAAuB;EAC9B,MAAM,UAAQ,SAAS;EACvB,IAAI,CAAC,SAAO,IACV;EAGF,QAAM,GAAG,SAAQ,UAAS;GACxB,IAAI,OAAO,MAAM,WAAW,YAC1B,MAAM,OAAO;EAEjB,CAAC;EAED,cAAc;CAChB;CAEA,cAAc;CAEd,CAAA,GAAA,IAAA,YAAA,OAAkB;EAChB,eAAe;CACjB,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -28,4 +28,3 @@ export declare function useStoreDevtools(options?: {
28
28
  */
29
29
  maxLogEntries?: number;
30
30
  }): UseStoreDevtoolsReturn;
31
- //# sourceMappingURL=useStoreDevtools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStoreDevtools.d.cts","sourceRoot":"","sources":["../../src/composables/useStoreDevtools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAe9B,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAA;IACxB,QAAQ,EAAE,GAAG,CAAC,aAAa,EAAE,CAAC,CAAA;IAC9B,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IAC1B,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC7D,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IACnC,cAAc,EAAE,MAAM,IAAI,CAAA;CAC3B;AAwBD,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE;IACP;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CAClB,GACL,sBAAsB,CAkHxB"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/store-vue v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:39:29 *
5
+ * Build time: 2024-10-12 17:40:04 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -78,6 +78,7 @@ function useStoreSnapshot(state, setState, options = {}) {
78
78
  const currentIndex = (0, vue.ref)(-1);
79
79
  let debounceTimer;
80
80
  let isRestoring = false;
81
+ let previousAutoRecordState;
81
82
  const getState = () => {
82
83
  return typeof state === "function" ? state() : state.value;
83
84
  };
@@ -108,6 +109,7 @@ function useStoreSnapshot(state, setState, options = {}) {
108
109
  try {
109
110
  setState((0, _composy_store_core.deepClone)(snapshots.value[index].state));
110
111
  currentIndex.value = index;
112
+ previousAutoRecordState = (0, _composy_store_core.deepClone)(getState());
111
113
  return true;
112
114
  } finally {
113
115
  isRestoring = false;
@@ -115,10 +117,12 @@ function useStoreSnapshot(state, setState, options = {}) {
115
117
  };
116
118
  const undo = () => {
117
119
  if (!canUndo.value) return false;
118
- currentIndex.value--;
120
+ const targetIndex = currentIndex.value - 1;
119
121
  isRestoring = true;
120
122
  try {
121
- setState((0, _composy_store_core.deepClone)(snapshots.value[currentIndex.value].state));
123
+ setState((0, _composy_store_core.deepClone)(snapshots.value[targetIndex].state));
124
+ currentIndex.value = targetIndex;
125
+ previousAutoRecordState = (0, _composy_store_core.deepClone)(getState());
122
126
  return true;
123
127
  } finally {
124
128
  isRestoring = false;
@@ -126,10 +130,12 @@ function useStoreSnapshot(state, setState, options = {}) {
126
130
  };
127
131
  const redo = () => {
128
132
  if (!canRedo.value) return false;
129
- currentIndex.value++;
133
+ const targetIndex = currentIndex.value + 1;
130
134
  isRestoring = true;
131
135
  try {
132
- setState((0, _composy_store_core.deepClone)(snapshots.value[currentIndex.value].state));
136
+ setState((0, _composy_store_core.deepClone)(snapshots.value[targetIndex].state));
137
+ currentIndex.value = targetIndex;
138
+ previousAutoRecordState = (0, _composy_store_core.deepClone)(getState());
133
139
  return true;
134
140
  } finally {
135
141
  isRestoring = false;
@@ -137,10 +143,11 @@ function useStoreSnapshot(state, setState, options = {}) {
137
143
  };
138
144
  const goto = (index) => {
139
145
  if (index < 0 || index >= snapshots.value.length) return false;
140
- currentIndex.value = index;
141
146
  isRestoring = true;
142
147
  try {
143
148
  setState((0, _composy_store_core.deepClone)(snapshots.value[index].state));
149
+ currentIndex.value = index;
150
+ previousAutoRecordState = (0, _composy_store_core.deepClone)(getState());
144
151
  return true;
145
152
  } finally {
146
153
  isRestoring = false;
@@ -175,9 +182,12 @@ function useStoreSnapshot(state, setState, options = {}) {
175
182
  }
176
183
  };
177
184
  if (autoRecord) {
178
- const stopWatch = (0, vue.watch)(typeof state === "function" ? (0, vue.computed)(state) : state, (newState, oldState) => {
185
+ const stateRef = typeof state === "function" ? (0, vue.computed)(state) : state;
186
+ previousAutoRecordState = (0, _composy_store_core.deepClone)(getState());
187
+ const stopWatch = (0, vue.watch)(stateRef, (newState, oldState) => {
179
188
  if (isRestoring) return;
180
- if ((0, _composy_store_core.deepEqual)(newState, oldState)) return;
189
+ if ((0, _composy_store_core.deepEqual)(newState, previousAutoRecordState ?? oldState)) return;
190
+ previousAutoRecordState = (0, _composy_store_core.deepClone)(newState);
181
191
  if (debounceTimer) clearTimeout(debounceTimer);
182
192
  debounceTimer = setTimeout(() => {
183
193
  takeSnapshot();
@@ -1 +1 @@
1
- {"version":3,"file":"useStoreSnapshot.cjs","names":[],"sources":["../../src/composables/useStoreSnapshot.ts"],"sourcesContent":["/**\n * useStoreSnapshot - Store 状态快照工具\n *\n * 提供状态快照、历史记录和调试功能\n * @module composables/useStoreSnapshot\n */\n\nimport type {ComputedRef, Ref} from 'vue';\nimport { deepClone, deepEqual } from '@composy/store-core'\nimport { computed, onUnmounted, ref, watch } from 'vue'\n\n/**\n * 状态快照\n */\nexport interface StateSnapshot<S = unknown> {\n /** 快照 ID */\n id: string\n /** 状态数据 */\n state: S\n /** 时间戳 */\n timestamp: number\n /** 快照标签 */\n label?: string\n /** 元数据 */\n meta?: Record<string, unknown>\n}\n\n/**\n * 状态差异\n */\nexport interface StateDiff {\n /** 差异路径 */\n path: string\n /** 旧值 */\n oldValue: unknown\n /** 新值 */\n newValue: unknown\n /** 差异类型 */\n type: 'add' | 'remove' | 'change'\n}\n\n/**\n * useStoreSnapshot 选项\n */\nexport interface UseStoreSnapshotOptions {\n /** 最大快照数量 */\n maxSnapshots?: number\n /** 是否自动记录 */\n autoRecord?: boolean\n /** 自动记录防抖延迟(毫秒) */\n debounceMs?: number\n /** 是否深度监听 */\n deep?: boolean\n}\n\n/**\n * useStoreSnapshot 返回值\n */\nexport interface UseStoreSnapshotReturn<S> {\n /** 快照列表 */\n snapshots: Ref<Array<StateSnapshot<S>>>\n /** 当前快照索引 */\n currentIndex: Ref<number>\n /** 是否可以撤销 */\n canUndo: ComputedRef<boolean>\n /** 是否可以重做 */\n canRedo: ComputedRef<boolean>\n /** 创建快照 */\n takeSnapshot: (label?: string, meta?: Record<string, unknown>) => StateSnapshot<S>\n /** 恢复快照 */\n restoreSnapshot: (id: string) => boolean\n /** 撤销 */\n undo: () => boolean\n /** 重做 */\n redo: () => boolean\n /** 跳转到指定快照 */\n goto: (index: number) => boolean\n /** 清空快照 */\n clearSnapshots: () => void\n /** 获取状态差异 */\n getDiff: (snapshotId1: string, snapshotId2: string) => StateDiff[]\n /** 导出快照 */\n exportSnapshots: () => string\n /** 导入快照 */\n importSnapshots: (data: string) => boolean\n}\n\n/**\n * 生成唯一 ID\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`\n}\n\n/**\n * 计算两个对象的差异\n */\nfunction computeDiff(\n oldObj: unknown,\n newObj: unknown,\n path = '',\n): StateDiff[] {\n const diffs: StateDiff[] = []\n\n // 类型不同\n if (typeof oldObj !== typeof newObj) {\n diffs.push({\n path: path || '.',\n oldValue: oldObj,\n newValue: newObj,\n type: 'change',\n })\n return diffs\n }\n\n // 基础类型比较\n if (typeof oldObj !== 'object' || oldObj === null || newObj === null) {\n if (!Object.is(oldObj, newObj)) {\n diffs.push({\n path: path || '.',\n oldValue: oldObj,\n newValue: newObj,\n type: 'change',\n })\n }\n return diffs\n }\n\n // 数组比较\n if (Array.isArray(oldObj) && Array.isArray(newObj)) {\n const maxLen = Math.max(oldObj.length, newObj.length)\n for (let i = 0; i < maxLen; i++) {\n const itemPath = path ? `${path}[${i}]` : `[${i}]`\n if (i >= oldObj.length) {\n diffs.push({\n path: itemPath,\n oldValue: undefined,\n newValue: newObj[i],\n type: 'add',\n })\n }\n else if (i >= newObj.length) {\n diffs.push({\n path: itemPath,\n oldValue: oldObj[i],\n newValue: undefined,\n type: 'remove',\n })\n }\n else {\n diffs.push(...computeDiff(oldObj[i], newObj[i], itemPath))\n }\n }\n return diffs\n }\n\n // 对象比较\n const oldKeys = new Set(Object.keys(oldObj as Record<string, unknown>))\n const newKeys = new Set(Object.keys(newObj as Record<string, unknown>))\n\n // 检查新增的键\n for (const key of newKeys) {\n if (!oldKeys.has(key)) {\n diffs.push({\n path: path ? `${path}.${key}` : key,\n oldValue: undefined,\n newValue: (newObj as Record<string, unknown>)[key],\n type: 'add',\n })\n }\n }\n\n // 检查删除的键\n for (const key of oldKeys) {\n if (!newKeys.has(key)) {\n diffs.push({\n path: path ? `${path}.${key}` : key,\n oldValue: (oldObj as Record<string, unknown>)[key],\n newValue: undefined,\n type: 'remove',\n })\n }\n }\n\n // 检查修改的键\n for (const key of oldKeys) {\n if (newKeys.has(key)) {\n diffs.push(\n ...computeDiff(\n (oldObj as Record<string, unknown>)[key],\n (newObj as Record<string, unknown>)[key],\n path ? `${path}.${key}` : key,\n ),\n )\n }\n }\n\n return diffs\n}\n\n/**\n * Store 状态快照 Hook\n *\n * 提供状态快照、撤销/重做和调试功能\n *\n * @template S - 状态类型\n * @param state - 响应式状态\n * @param setState - 状态设置函数\n * @param options - 配置选项\n * @returns 快照操作对象\n *\n * @example\n * ```typescript\n * const store = useCounterStore()\n *\n * const {\n * snapshots,\n * canUndo,\n * canRedo,\n * takeSnapshot,\n * undo,\n * redo,\n * getDiff\n * } = useStoreSnapshot(\n * () => store.$state,\n * (state) => store.$patch(state),\n * { maxSnapshots: 50, autoRecord: true }\n * )\n *\n * // 手动创建快照\n * takeSnapshot('保存点 1')\n *\n * // 撤销操作\n * if (canUndo.value) {\n * undo()\n * }\n *\n * // 获取状态差异\n * const diff = getDiff(snapshots.value[0].id, snapshots.value[1].id)\n * ```\n */\nexport function useStoreSnapshot<S>(\n state: Ref<S> | (() => S),\n setState: (state: S) => void,\n options: UseStoreSnapshotOptions = {},\n): UseStoreSnapshotReturn<S> {\n const {\n maxSnapshots = 100,\n autoRecord = false,\n debounceMs = 300,\n deep = true,\n } = options\n\n const snapshots = ref<Array<StateSnapshot<S>>>([]) as Ref<Array<StateSnapshot<S>>>\n const currentIndex = ref(-1)\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n let isRestoring = false\n\n // 获取当前状态\n const getState = (): S => {\n return typeof state === 'function' ? state() : state.value\n }\n\n // 是否可以撤销\n const canUndo = computed(() => currentIndex.value > 0)\n\n // 是否可以重做\n const canRedo = computed(() => currentIndex.value < snapshots.value.length - 1)\n\n /**\n * 创建快照\n */\n const takeSnapshot = (label?: string, meta?: Record<string, unknown>): StateSnapshot<S> => {\n const currentState = getState()\n const snapshot: StateSnapshot<S> = {\n id: generateId(),\n state: deepClone(currentState),\n timestamp: Date.now(),\n label,\n meta,\n }\n\n // 如果当前不在最新位置,删除后面的快照\n if (currentIndex.value < snapshots.value.length - 1) {\n snapshots.value = snapshots.value.slice(0, currentIndex.value + 1)\n }\n\n // 添加新快照\n snapshots.value.push(snapshot)\n currentIndex.value = snapshots.value.length - 1\n\n // 限制快照数量\n if (snapshots.value.length > maxSnapshots) {\n snapshots.value.shift()\n currentIndex.value--\n }\n\n return snapshot\n }\n\n /**\n * 恢复快照\n */\n const restoreSnapshot = (id: string): boolean => {\n const index = snapshots.value.findIndex(s => s.id === id)\n if (index === -1) return false\n\n isRestoring = true\n try {\n setState(deepClone(snapshots.value[index].state))\n currentIndex.value = index\n return true\n }\n finally {\n isRestoring = false\n }\n }\n\n /**\n * 撤销\n */\n const undo = (): boolean => {\n if (!canUndo.value) return false\n\n currentIndex.value--\n isRestoring = true\n try {\n setState(deepClone(snapshots.value[currentIndex.value].state))\n return true\n }\n finally {\n isRestoring = false\n }\n }\n\n /**\n * 重做\n */\n const redo = (): boolean => {\n if (!canRedo.value) return false\n\n currentIndex.value++\n isRestoring = true\n try {\n setState(deepClone(snapshots.value[currentIndex.value].state))\n return true\n }\n finally {\n isRestoring = false\n }\n }\n\n /**\n * 跳转到指定快照\n */\n const goto = (index: number): boolean => {\n if (index < 0 || index >= snapshots.value.length) return false\n\n currentIndex.value = index\n isRestoring = true\n try {\n setState(deepClone(snapshots.value[index].state))\n return true\n }\n finally {\n isRestoring = false\n }\n }\n\n /**\n * 清空快照\n */\n const clearSnapshots = (): void => {\n snapshots.value = []\n currentIndex.value = -1\n }\n\n /**\n * 获取状态差异\n */\n const getDiff = (snapshotId1: string, snapshotId2: string): StateDiff[] => {\n const snapshot1 = snapshots.value.find(s => s.id === snapshotId1)\n const snapshot2 = snapshots.value.find(s => s.id === snapshotId2)\n\n if (!snapshot1 || !snapshot2) return []\n\n return computeDiff(snapshot1.state, snapshot2.state)\n }\n\n /**\n * 导出快照\n */\n const exportSnapshots = (): string => {\n return JSON.stringify({\n snapshots: snapshots.value,\n currentIndex: currentIndex.value,\n exportedAt: Date.now(),\n })\n }\n\n /**\n * 导入快照\n */\n const importSnapshots = (data: string): boolean => {\n try {\n const parsed = JSON.parse(data) as {\n snapshots: Array<StateSnapshot<S>>\n currentIndex: number\n }\n\n if (!Array.isArray(parsed.snapshots)) return false\n\n snapshots.value = parsed.snapshots\n currentIndex.value = Math.min(\n parsed.currentIndex ?? snapshots.value.length - 1,\n snapshots.value.length - 1,\n )\n\n return true\n }\n catch {\n return false\n }\n }\n\n // 自动记录\n if (autoRecord) {\n const stateRef = typeof state === 'function' ? computed(state) : state\n\n const stopWatch = watch(\n stateRef,\n (newState, oldState) => {\n // 如果是恢复操作,不记录\n if (isRestoring) return\n\n // 如果状态未变化,不记录\n if (deepEqual(newState, oldState)) return\n\n // 防抖记录\n if (debounceTimer) {\n clearTimeout(debounceTimer)\n }\n\n debounceTimer = setTimeout(() => {\n takeSnapshot()\n }, debounceMs)\n },\n { deep },\n )\n\n // 初始快照\n takeSnapshot('初始状态')\n\n onUnmounted(() => {\n stopWatch()\n if (debounceTimer) {\n clearTimeout(debounceTimer)\n }\n })\n }\n\n return {\n snapshots,\n currentIndex,\n canUndo,\n canRedo,\n takeSnapshot,\n restoreSnapshot,\n undo,\n redo,\n goto,\n clearSnapshots,\n getDiff,\n exportSnapshots,\n importSnapshots,\n }\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"useStoreSnapshot.cjs","names":[],"sources":["../../src/composables/useStoreSnapshot.ts"],"sourcesContent":["/**\r\n * useStoreSnapshot - Store 状态快照工具\r\n *\r\n * 提供状态快照、历史记录和调试功能\r\n * @module composables/useStoreSnapshot\r\n */\r\n\r\nimport type { ComputedRef, Ref } from 'vue'\r\nimport { deepClone, deepEqual } from '@composy/store-core'\r\nimport { computed, onUnmounted, ref, watch } from 'vue'\r\n\r\n/**\r\n * 状态快照\r\n */\r\nexport interface StateSnapshot<S = unknown> {\r\n /** 快照 ID */\r\n id: string\r\n /** 状态数据 */\r\n state: S\r\n /** 时间戳 */\r\n timestamp: number\r\n /** 快照标签 */\r\n label?: string\r\n /** 元数据 */\r\n meta?: Record<string, unknown>\r\n}\r\n\r\n/**\r\n * 状态差异\r\n */\r\nexport interface StateDiff {\r\n /** 差异路径 */\r\n path: string\r\n /** 旧值 */\r\n oldValue: unknown\r\n /** 新值 */\r\n newValue: unknown\r\n /** 差异类型 */\r\n type: 'add' | 'remove' | 'change'\r\n}\r\n\r\n/**\r\n * useStoreSnapshot 选项\r\n */\r\nexport interface UseStoreSnapshotOptions {\r\n /** 最大快照数量 */\r\n maxSnapshots?: number\r\n /** 是否自动记录 */\r\n autoRecord?: boolean\r\n /** 自动记录防抖延迟(毫秒) */\r\n debounceMs?: number\r\n /** 是否深度监听 */\r\n deep?: boolean\r\n}\r\n\r\n/**\r\n * useStoreSnapshot 返回值\r\n */\r\nexport interface UseStoreSnapshotReturn<S> {\r\n /** 快照列表 */\r\n snapshots: Ref<Array<StateSnapshot<S>>>\r\n /** 当前快照索引 */\r\n currentIndex: Ref<number>\r\n /** 是否可以撤销 */\r\n canUndo: ComputedRef<boolean>\r\n /** 是否可以重做 */\r\n canRedo: ComputedRef<boolean>\r\n /** 创建快照 */\r\n takeSnapshot: (\r\n label?: string,\r\n meta?: Record<string, unknown>\r\n ) => StateSnapshot<S>\r\n /** 恢复快照 */\r\n restoreSnapshot: (id: string) => boolean\r\n /** 撤销 */\r\n undo: () => boolean\r\n /** 重做 */\r\n redo: () => boolean\r\n /** 跳转到指定快照 */\r\n goto: (index: number) => boolean\r\n /** 清空快照 */\r\n clearSnapshots: () => void\r\n /** 获取状态差异 */\r\n getDiff: (snapshotId1: string, snapshotId2: string) => StateDiff[]\r\n /** 导出快照 */\r\n exportSnapshots: () => string\r\n /** 导入快照 */\r\n importSnapshots: (data: string) => boolean\r\n}\r\n\r\n/**\r\n * 生成唯一 ID\r\n */\r\nfunction generateId(): string {\r\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`\r\n}\r\n\r\n/**\r\n * 计算两个对象的差异\r\n */\r\nfunction computeDiff(oldObj: unknown, newObj: unknown, path = ''): StateDiff[] {\r\n const diffs: StateDiff[] = []\r\n\r\n // 类型不同\r\n if (typeof oldObj !== typeof newObj) {\r\n diffs.push({\r\n path: path || '.',\r\n oldValue: oldObj,\r\n newValue: newObj,\r\n type: 'change',\r\n })\r\n return diffs\r\n }\r\n\r\n // 基础类型比较\r\n if (typeof oldObj !== 'object' || oldObj === null || newObj === null) {\r\n if (!Object.is(oldObj, newObj)) {\r\n diffs.push({\r\n path: path || '.',\r\n oldValue: oldObj,\r\n newValue: newObj,\r\n type: 'change',\r\n })\r\n }\r\n return diffs\r\n }\r\n\r\n // 数组比较\r\n if (Array.isArray(oldObj) && Array.isArray(newObj)) {\r\n const maxLen = Math.max(oldObj.length, newObj.length)\r\n for (let i = 0; i < maxLen; i++) {\r\n const itemPath = path ? `${path}[${i}]` : `[${i}]`\r\n if (i >= oldObj.length) {\r\n diffs.push({\r\n path: itemPath,\r\n oldValue: undefined,\r\n newValue: newObj[i],\r\n type: 'add',\r\n })\r\n } else if (i >= newObj.length) {\r\n diffs.push({\r\n path: itemPath,\r\n oldValue: oldObj[i],\r\n newValue: undefined,\r\n type: 'remove',\r\n })\r\n } else {\r\n diffs.push(...computeDiff(oldObj[i], newObj[i], itemPath))\r\n }\r\n }\r\n return diffs\r\n }\r\n\r\n // 对象比较\r\n const oldKeys = new Set(Object.keys(oldObj as Record<string, unknown>))\r\n const newKeys = new Set(Object.keys(newObj as Record<string, unknown>))\r\n\r\n // 检查新增的键\r\n for (const key of newKeys) {\r\n if (!oldKeys.has(key)) {\r\n diffs.push({\r\n path: path ? `${path}.${key}` : key,\r\n oldValue: undefined,\r\n newValue: (newObj as Record<string, unknown>)[key],\r\n type: 'add',\r\n })\r\n }\r\n }\r\n\r\n // 检查删除的键\r\n for (const key of oldKeys) {\r\n if (!newKeys.has(key)) {\r\n diffs.push({\r\n path: path ? `${path}.${key}` : key,\r\n oldValue: (oldObj as Record<string, unknown>)[key],\r\n newValue: undefined,\r\n type: 'remove',\r\n })\r\n }\r\n }\r\n\r\n // 检查修改的键\r\n for (const key of oldKeys) {\r\n if (newKeys.has(key)) {\r\n diffs.push(\r\n ...computeDiff(\r\n (oldObj as Record<string, unknown>)[key],\r\n (newObj as Record<string, unknown>)[key],\r\n path ? `${path}.${key}` : key\r\n )\r\n )\r\n }\r\n }\r\n\r\n return diffs\r\n}\r\n\r\n/**\r\n * Store 状态快照 Hook\r\n *\r\n * 提供状态快照、撤销/重做和调试功能\r\n *\r\n * @template S - 状态类型\r\n * @param state - 响应式状态\r\n * @param setState - 状态设置函数\r\n * @param options - 配置选项\r\n * @returns 快照操作对象\r\n *\r\n * @example\r\n * ```typescript\r\n * const store = useCounterStore()\r\n *\r\n * const {\r\n * snapshots,\r\n * canUndo,\r\n * canRedo,\r\n * takeSnapshot,\r\n * undo,\r\n * redo,\r\n * getDiff\r\n * } = useStoreSnapshot(\r\n * () => store.$state,\r\n * (state) => store.$patch(state),\r\n * { maxSnapshots: 50, autoRecord: true }\r\n * )\r\n *\r\n * // 手动创建快照\r\n * takeSnapshot('保存点 1')\r\n *\r\n * // 撤销操作\r\n * if (canUndo.value) {\r\n * undo()\r\n * }\r\n *\r\n * // 获取状态差异\r\n * const diff = getDiff(snapshots.value[0].id, snapshots.value[1].id)\r\n * ```\r\n */\r\nexport function useStoreSnapshot<S>(\r\n state: Ref<S> | (() => S),\r\n setState: (state: S) => void,\r\n options: UseStoreSnapshotOptions = {}\r\n): UseStoreSnapshotReturn<S> {\r\n const {\r\n maxSnapshots = 100,\r\n autoRecord = false,\r\n debounceMs = 300,\r\n deep = true,\r\n } = options\r\n\r\n const snapshots = ref<Array<StateSnapshot<S>>>([]) as Ref<\r\n Array<StateSnapshot<S>>\r\n >\r\n const currentIndex = ref(-1)\r\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\r\n let isRestoring = false\r\n let previousAutoRecordState: S | undefined\r\n\r\n // 获取当前状态\r\n const getState = (): S => {\r\n return typeof state === 'function' ? state() : state.value\r\n }\r\n\r\n // 是否可以撤销\r\n const canUndo = computed(() => currentIndex.value > 0)\r\n\r\n // 是否可以重做\r\n const canRedo = computed(\r\n () => currentIndex.value < snapshots.value.length - 1\r\n )\r\n\r\n /**\r\n * 创建快照\r\n */\r\n const takeSnapshot = (\r\n label?: string,\r\n meta?: Record<string, unknown>\r\n ): StateSnapshot<S> => {\r\n const currentState = getState()\r\n const snapshot: StateSnapshot<S> = {\r\n id: generateId(),\r\n state: deepClone(currentState),\r\n timestamp: Date.now(),\r\n label,\r\n meta,\r\n }\r\n\r\n // 如果当前不在最新位置,删除后面的快照\r\n if (currentIndex.value < snapshots.value.length - 1) {\r\n snapshots.value = snapshots.value.slice(0, currentIndex.value + 1)\r\n }\r\n\r\n // 添加新快照\r\n snapshots.value.push(snapshot)\r\n currentIndex.value = snapshots.value.length - 1\r\n\r\n // 限制快照数量\r\n if (snapshots.value.length > maxSnapshots) {\r\n snapshots.value.shift()\r\n currentIndex.value--\r\n }\r\n\r\n return snapshot\r\n }\r\n\r\n /**\r\n * 恢复快照\r\n */\r\n const restoreSnapshot = (id: string): boolean => {\r\n const index = snapshots.value.findIndex(s => s.id === id)\r\n if (index === -1) return false\r\n\r\n isRestoring = true\r\n try {\r\n setState(deepClone(snapshots.value[index].state))\r\n currentIndex.value = index\r\n previousAutoRecordState = deepClone(getState())\r\n return true\r\n } finally {\r\n isRestoring = false\r\n }\r\n }\r\n\r\n /**\r\n * 撤销\r\n */\r\n const undo = (): boolean => {\r\n if (!canUndo.value) return false\r\n\r\n const targetIndex = currentIndex.value - 1\r\n isRestoring = true\r\n try {\r\n setState(deepClone(snapshots.value[targetIndex].state))\r\n currentIndex.value = targetIndex\r\n previousAutoRecordState = deepClone(getState())\r\n return true\r\n } finally {\r\n isRestoring = false\r\n }\r\n }\r\n\r\n /**\r\n * 重做\r\n */\r\n const redo = (): boolean => {\r\n if (!canRedo.value) return false\r\n\r\n const targetIndex = currentIndex.value + 1\r\n isRestoring = true\r\n try {\r\n setState(deepClone(snapshots.value[targetIndex].state))\r\n currentIndex.value = targetIndex\r\n previousAutoRecordState = deepClone(getState())\r\n return true\r\n } finally {\r\n isRestoring = false\r\n }\r\n }\r\n\r\n /**\r\n * 跳转到指定快照\r\n */\r\n const goto = (index: number): boolean => {\r\n if (index < 0 || index >= snapshots.value.length) return false\r\n\r\n isRestoring = true\r\n try {\r\n setState(deepClone(snapshots.value[index].state))\r\n currentIndex.value = index\r\n previousAutoRecordState = deepClone(getState())\r\n return true\r\n } finally {\r\n isRestoring = false\r\n }\r\n }\r\n\r\n /**\r\n * 清空快照\r\n */\r\n const clearSnapshots = (): void => {\r\n snapshots.value = []\r\n currentIndex.value = -1\r\n }\r\n\r\n /**\r\n * 获取状态差异\r\n */\r\n const getDiff = (snapshotId1: string, snapshotId2: string): StateDiff[] => {\r\n const snapshot1 = snapshots.value.find(s => s.id === snapshotId1)\r\n const snapshot2 = snapshots.value.find(s => s.id === snapshotId2)\r\n\r\n if (!snapshot1 || !snapshot2) return []\r\n\r\n return computeDiff(snapshot1.state, snapshot2.state)\r\n }\r\n\r\n /**\r\n * 导出快照\r\n */\r\n const exportSnapshots = (): string => {\r\n return JSON.stringify({\r\n snapshots: snapshots.value,\r\n currentIndex: currentIndex.value,\r\n exportedAt: Date.now(),\r\n })\r\n }\r\n\r\n /**\r\n * 导入快照\r\n */\r\n const importSnapshots = (data: string): boolean => {\r\n try {\r\n const parsed = JSON.parse(data) as {\r\n snapshots: Array<StateSnapshot<S>>\r\n currentIndex: number\r\n }\r\n\r\n if (!Array.isArray(parsed.snapshots)) return false\r\n\r\n snapshots.value = parsed.snapshots\r\n currentIndex.value = Math.min(\r\n parsed.currentIndex ?? snapshots.value.length - 1,\r\n snapshots.value.length - 1\r\n )\r\n\r\n return true\r\n } catch {\r\n return false\r\n }\r\n }\r\n\r\n // 自动记录\r\n if (autoRecord) {\r\n const stateRef = typeof state === 'function' ? computed(state) : state\r\n previousAutoRecordState = deepClone(getState())\r\n\r\n const stopWatch = watch(\r\n stateRef,\r\n (newState, oldState) => {\r\n // 如果是恢复操作,不记录\r\n if (isRestoring) return\r\n\r\n // 如果状态未变化,不记录\r\n if (deepEqual(newState, previousAutoRecordState ?? oldState)) return\r\n previousAutoRecordState = deepClone(newState)\r\n\r\n // 防抖记录\r\n if (debounceTimer) {\r\n clearTimeout(debounceTimer)\r\n }\r\n\r\n debounceTimer = setTimeout(() => {\r\n takeSnapshot()\r\n }, debounceMs)\r\n },\r\n { deep }\r\n )\r\n\r\n // 初始快照\r\n takeSnapshot('初始状态')\r\n\r\n onUnmounted(() => {\r\n stopWatch()\r\n if (debounceTimer) {\r\n clearTimeout(debounceTimer)\r\n }\r\n })\r\n }\r\n\r\n return {\r\n snapshots,\r\n currentIndex,\r\n canUndo,\r\n canRedo,\r\n takeSnapshot,\r\n restoreSnapshot,\r\n undo,\r\n redo,\r\n goto,\r\n clearSnapshots,\r\n getDiff,\r\n exportSnapshots,\r\n importSnapshots,\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -119,4 +119,3 @@ export interface UseStoreSnapshotReturn<S> {
119
119
  * ```
120
120
  */
121
121
  export declare function useStoreSnapshot<S>(state: Ref<S> | (() => S), setState: (state: S) => void, options?: UseStoreSnapshotOptions): UseStoreSnapshotReturn<S>;
122
- //# sourceMappingURL=useStoreSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStoreSnapshot.d.cts","sourceRoot":"","sources":["../../src/composables/useStoreSnapshot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAI3C;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,YAAY;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,WAAW;IACX,KAAK,EAAE,CAAC,CAAA;IACR,UAAU;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU;IACV,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,WAAW;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS;IACT,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS;IACT,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW;IACX,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,aAAa;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa;IACb,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,WAAW;IACX,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACvC,aAAa;IACb,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,aAAa;IACb,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IAC7B,aAAa;IACb,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IAC7B,WAAW;IACX,YAAY,EAAE,CACZ,KAAK,CAAC,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,aAAa,CAAC,CAAC,CAAC,CAAA;IACrB,WAAW;IACX,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IACxC,SAAS;IACT,IAAI,EAAE,MAAM,OAAO,CAAA;IACnB,SAAS;IACT,IAAI,EAAE,MAAM,OAAO,CAAA;IACnB,cAAc;IACd,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;IAChC,WAAW;IACX,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,aAAa;IACb,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,SAAS,EAAE,CAAA;IAClE,WAAW;IACX,eAAe,EAAE,MAAM,MAAM,CAAA;IAC7B,WAAW;IACX,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAC3C;AA6GD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EAC5B,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAAC,CAAC,CAAC,CAkP3B"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/store-vue v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:39:29 *
5
+ * Build time: 2024-10-12 17:40:04 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -1 +1 @@
1
- {"version":3,"file":"useSubscribe.cjs","names":[],"sources":["../../src/composables/useSubscribe.ts"],"sourcesContent":["/**\n * useSubscribe Composable\n *\n * 提供 Store 订阅功能\n * @module composables/useSubscribe\n */\n\nimport type { Ref, WatchStopHandle } from 'vue'\nimport type { MutationInfo, SubscribeOptions } from '../types'\nimport { onUnmounted, watch } from 'vue'\n\n/**\n * 订阅 Store 状态变化\n *\n * @template S - 状态类型\n * @param store - Pinia Store 实例\n * @param callback - 回调函数\n * @param options - 订阅选项\n * @returns 取消订阅函数\n *\n * @example\n * ```typescript\n * const store = useCounterStore()\n *\n * // 订阅状态变化\n * const unsubscribe = useSubscribe(store, (mutation, state) => {\n * console.log('State changed:', mutation.type, state)\n * })\n *\n * // 取消订阅\n * unsubscribe()\n * ```\n */\nexport function useSubscribe<S extends Record<string, unknown>>(\n store: S & { $subscribe?: (callback: (mutation: MutationInfo<S>, state: S) => void, options?: { detached?: boolean }) => () => void },\n callback: (mutation: MutationInfo<S>, state: S) => void,\n options: SubscribeOptions = {},\n): () => void {\n const { detached = false } = options\n\n // 使用 Pinia 的 $subscribe\n if (store.$subscribe) {\n const unsubscribe = store.$subscribe(\n (mutation, state) => {\n callback(mutation as MutationInfo<S>, state as S)\n },\n { detached },\n )\n\n // 组件卸载时自动取消订阅\n if (!detached) {\n onUnmounted(() => {\n unsubscribe()\n })\n }\n\n return unsubscribe\n }\n\n // 如果不是 Pinia Store,返回空函数\n console.warn('[useSubscribe] Store 不支持 $subscribe 方法')\n return () => {}\n}\n\n/**\n * 订阅 Store Action 调用\n *\n * @template S - Store 类型\n * @param store - Pinia Store 实例\n * @param callback - 回调函数\n * @returns 取消订阅函数\n *\n * @example\n * ```typescript\n * const store = useCounterStore()\n *\n * // 订阅 Action 调用\n * useOnAction(store, ({ name, args, after, onError }) => {\n * console.log(`Action ${name} called with:`, args)\n *\n * after((result) => {\n * console.log(`Action ${name} finished with:`, result)\n * })\n *\n * onError((error) => {\n * console.error(`Action ${name} failed:`, error)\n * })\n * })\n * ```\n */\nexport function useOnAction<S extends Record<string, unknown>>(\n store: S & { $onAction?: (callback: (context: ActionContext) => void, detached?: boolean) => () => void },\n callback: (context: ActionContext) => void,\n detached = false,\n): () => void {\n if (store.$onAction) {\n const unsubscribe = store.$onAction(callback, detached)\n\n if (!detached) {\n onUnmounted(() => {\n unsubscribe()\n })\n }\n\n return unsubscribe\n }\n\n console.warn('[useOnAction] Store 不支持 $onAction 方法')\n return () => {}\n}\n\n/**\n * Action 上下文\n */\nexport interface ActionContext {\n /** Action 名称 */\n name: string\n /** Store 实例 */\n store: unknown\n /** Action 参数 */\n args: unknown[]\n /** Action 完成后回调 */\n after: (callback: (result: unknown) => void) => void\n /** Action 错误回调 */\n onError: (callback: (error: unknown) => void) => void\n}\n\n/**\n * 监听 ref 变化\n *\n * @template T - 值类型\n * @param source - 监听源\n * @param callback - 回调函数\n * @param options - 监听选项\n * @returns 停止监听函数\n */\nexport function useWatch<T>(\n source: Ref<T>,\n callback: (newValue: T, oldValue: T | undefined) => void,\n options: SubscribeOptions = {},\n): WatchStopHandle {\n const { immediate = false, deep = false, detached = false } = options\n\n const stop = watch(\n source,\n (newValue, oldValue) => {\n callback(newValue, oldValue)\n },\n { immediate, deep },\n )\n\n if (!detached) {\n onUnmounted(() => {\n stop()\n })\n }\n\n return stop\n}\n\n"],"mappings":";;;;;;;;;;;;;AAiCO,SAAS,aACd,OACA,UACA,UAA4B,CAAC,GACjB;CACZ,MAAM,EAAE,WAAW,UAAU;CAG7B,IAAI,MAAM,YAAY;EACpB,MAAM,cAAc,MAAM,YACvB,UAAU,UAAU;GACnB,SAAS,UAA6B,KAAU;EAClD,GACA,EAAE,SAAS,CACb;EAGA,IAAI,CAAC,UACH,CAAA,GAAA,IAAA,YAAA,OAAkB;GAChB,YAAY;EACd,CAAC;EAGH,OAAO;CACT;CAGA,QAAQ,KAAK,wCAAwC;CACrD,aAAa,CAAC;AAChB;AA4BO,SAAS,YACd,OACA,UACA,WAAW,OACC;CACZ,IAAI,MAAM,WAAW;EACnB,MAAM,cAAc,MAAM,UAAU,UAAU,QAAQ;EAEtD,IAAI,CAAC,UACH,CAAA,GAAA,IAAA,YAAA,OAAkB;GAChB,YAAY;EACd,CAAC;EAGH,OAAO;CACT;CAEA,QAAQ,KAAK,sCAAsC;CACnD,aAAa,CAAC;AAChB;AA2BO,SAAS,SACd,QACA,UACA,UAA4B,CAAC,GACZ;CACjB,MAAM,EAAE,YAAY,OAAO,OAAO,OAAO,WAAW,UAAU;CAE9D,MAAM,QAAA,GAAA,IAAA,MAAA,CACJ,SACC,UAAU,aAAa;EACtB,SAAS,UAAU,QAAQ;CAC7B,GACA;EAAE;EAAW;CAAK,CACpB;CAEA,IAAI,CAAC,UACH,CAAA,GAAA,IAAA,YAAA,OAAkB;EAChB,KAAK;CACP,CAAC;CAGH,OAAO;AACT"}
1
+ {"version":3,"file":"useSubscribe.cjs","names":[],"sources":["../../src/composables/useSubscribe.ts"],"sourcesContent":["/**\r\n * useSubscribe Composable\r\n *\r\n * 提供 Store 订阅功能\r\n * @module composables/useSubscribe\r\n */\r\n\r\nimport type { Ref, WatchStopHandle } from 'vue'\r\nimport type { MutationInfo, SubscribeOptions } from '../types'\r\nimport { onUnmounted, watch } from 'vue'\r\n\r\n/**\r\n * 订阅 Store 状态变化\r\n *\r\n * @template S - 状态类型\r\n * @param store - Pinia Store 实例\r\n * @param callback - 回调函数\r\n * @param options - 订阅选项\r\n * @returns 取消订阅函数\r\n *\r\n * @example\r\n * ```typescript\r\n * const store = useCounterStore()\r\n *\r\n * // 订阅状态变化\r\n * const unsubscribe = useSubscribe(store, (mutation, state) => {\r\n * console.log('State changed:', mutation.type, state)\r\n * })\r\n *\r\n * // 取消订阅\r\n * unsubscribe()\r\n * ```\r\n */\r\nexport function useSubscribe<S extends Record<string, unknown>>(\r\n store: S & {\r\n $subscribe?: (\r\n callback: (mutation: MutationInfo<S>, state: S) => void,\r\n options?: { detached?: boolean }\r\n ) => () => void\r\n },\r\n callback: (mutation: MutationInfo<S>, state: S) => void,\r\n options: SubscribeOptions = {}\r\n): () => void {\r\n const { detached = false } = options\r\n\r\n // 使用 Pinia 的 $subscribe\r\n if (store.$subscribe) {\r\n const unsubscribe = store.$subscribe(\r\n (mutation, state) => {\r\n callback(mutation as MutationInfo<S>, state as S)\r\n },\r\n { detached }\r\n )\r\n\r\n // 组件卸载时自动取消订阅\r\n if (!detached) {\r\n onUnmounted(() => {\r\n unsubscribe()\r\n })\r\n }\r\n\r\n return unsubscribe\r\n }\r\n\r\n // 如果不是 Pinia Store,返回空函数\r\n console.warn('[useSubscribe] Store 不支持 $subscribe 方法')\r\n return () => {}\r\n}\r\n\r\n/**\r\n * 订阅 Store Action 调用\r\n *\r\n * @template S - Store 类型\r\n * @param store - Pinia Store 实例\r\n * @param callback - 回调函数\r\n * @returns 取消订阅函数\r\n *\r\n * @example\r\n * ```typescript\r\n * const store = useCounterStore()\r\n *\r\n * // 订阅 Action 调用\r\n * useOnAction(store, ({ name, args, after, onError }) => {\r\n * console.log(`Action ${name} called with:`, args)\r\n *\r\n * after((result) => {\r\n * console.log(`Action ${name} finished with:`, result)\r\n * })\r\n *\r\n * onError((error) => {\r\n * console.error(`Action ${name} failed:`, error)\r\n * })\r\n * })\r\n * ```\r\n */\r\nexport function useOnAction<S extends Record<string, unknown>>(\r\n store: S & {\r\n $onAction?: (\r\n callback: (context: ActionContext) => void,\r\n detached?: boolean\r\n ) => () => void\r\n },\r\n callback: (context: ActionContext) => void,\r\n detached = false\r\n): () => void {\r\n if (store.$onAction) {\r\n const unsubscribe = store.$onAction(callback, detached)\r\n\r\n if (!detached) {\r\n onUnmounted(() => {\r\n unsubscribe()\r\n })\r\n }\r\n\r\n return unsubscribe\r\n }\r\n\r\n console.warn('[useOnAction] Store 不支持 $onAction 方法')\r\n return () => {}\r\n}\r\n\r\n/**\r\n * Action 上下文\r\n */\r\nexport interface ActionContext {\r\n /** Action 名称 */\r\n name: string\r\n /** Store 实例 */\r\n store: unknown\r\n /** Action 参数 */\r\n args: unknown[]\r\n /** Action 完成后回调 */\r\n after: (callback: (result: unknown) => void) => void\r\n /** Action 错误回调 */\r\n onError: (callback: (error: unknown) => void) => void\r\n}\r\n\r\n/**\r\n * 监听 ref 变化\r\n *\r\n * @template T - 值类型\r\n * @param source - 监听源\r\n * @param callback - 回调函数\r\n * @param options - 监听选项\r\n * @returns 停止监听函数\r\n */\r\nexport function useWatch<T>(\r\n source: Ref<T>,\r\n callback: (newValue: T, oldValue: T | undefined) => void,\r\n options: SubscribeOptions = {}\r\n): WatchStopHandle {\r\n const { immediate = false, deep = false, detached = false } = options\r\n\r\n const stop = watch(\r\n source,\r\n (newValue, oldValue) => {\r\n callback(newValue, oldValue)\r\n },\r\n { immediate, deep }\r\n )\r\n\r\n if (!detached) {\r\n onUnmounted(() => {\r\n stop()\r\n })\r\n }\r\n\r\n return stop\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAiCO,SAAS,aACd,OAMA,UACA,UAA4B,CAAC,GACjB;CACZ,MAAM,EAAE,WAAW,UAAU;CAG7B,IAAI,MAAM,YAAY;EACpB,MAAM,cAAc,MAAM,YACvB,UAAU,UAAU;GACnB,SAAS,UAA6B,KAAU;EAClD,GACA,EAAE,SAAS,CACb;EAGA,IAAI,CAAC,UACH,CAAA,GAAA,IAAA,YAAA,OAAkB;GAChB,YAAY;EACd,CAAC;EAGH,OAAO;CACT;CAGA,QAAQ,KAAK,wCAAwC;CACrD,aAAa,CAAC;AAChB;AA4BO,SAAS,YACd,OAMA,UACA,WAAW,OACC;CACZ,IAAI,MAAM,WAAW;EACnB,MAAM,cAAc,MAAM,UAAU,UAAU,QAAQ;EAEtD,IAAI,CAAC,UACH,CAAA,GAAA,IAAA,YAAA,OAAkB;GAChB,YAAY;EACd,CAAC;EAGH,OAAO;CACT;CAEA,QAAQ,KAAK,sCAAsC;CACnD,aAAa,CAAC;AAChB;AA2BO,SAAS,SACd,QACA,UACA,UAA4B,CAAC,GACZ;CACjB,MAAM,EAAE,YAAY,OAAO,OAAO,OAAO,WAAW,UAAU;CAE9D,MAAM,QAAA,GAAA,IAAA,MAAA,CACJ,SACC,UAAU,aAAa;EACtB,SAAS,UAAU,QAAQ;CAC7B,GACA;EAAE;EAAW;CAAK,CACpB;CAEA,IAAI,CAAC,UACH,CAAA,GAAA,IAAA,YAAA,OAAkB;EAChB,KAAK;CACP,CAAC;CAGH,OAAO;AACT"}
@@ -87,4 +87,3 @@ export interface ActionContext {
87
87
  * @returns 停止监听函数
88
88
  */
89
89
  export declare function useWatch<T>(source: Ref<T>, callback: (newValue: T, oldValue: T | undefined) => void, options?: SubscribeOptions): WatchStopHandle;
90
- //# sourceMappingURL=useSubscribe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSubscribe.d.cts","sourceRoot":"","sources":["../../src/composables/useSubscribe.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,KAAK,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAG9D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,CAAC,GAAG;IACT,UAAU,CAAC,EAAE,CACX,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,EACvD,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAC7B,MAAM,IAAI,CAAA;CAChB,EACD,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,EACvD,OAAO,GAAE,gBAAqB,GAC7B,MAAM,IAAI,CAyBZ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,CAAC,GAAG;IACT,SAAS,CAAC,EAAE,CACV,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,EAC1C,QAAQ,CAAC,EAAE,OAAO,KACf,MAAM,IAAI,CAAA;CAChB,EACD,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,EAC1C,QAAQ,UAAQ,GACf,MAAM,IAAI,CAeZ;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe;IACf,KAAK,EAAE,OAAO,CAAA;IACd,gBAAgB;IAChB,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,mBAAmB;IACnB,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAA;IACpD,kBAAkB;IAClB,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAA;CACtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EACd,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,EACxD,OAAO,GAAE,gBAAqB,GAC7B,eAAe,CAkBjB"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/store-vue v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:39:29 *
5
+ * Build time: 2024-10-12 17:40:04 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -1 +1 @@
1
- {"version":3,"file":"injection-keys.cjs","names":[],"sources":["../../src/context/injection-keys.ts"],"sourcesContent":["// 从 pinia 导入 Pinia 类型\nimport type { Pinia } from 'pinia'\n// 从 vue 导入 InjectionKey 类型,用于类型安全的依赖注入\nimport type { InjectionKey } from 'vue'\n// 导入 StoreRuntime 类型,表示 store 运行时实例\nimport type { StoreRuntime } from '../runtime/store-runtime'\n// 导入 GlobalStoreAPI 类型,表示全局 store API 接口\nimport type { GlobalStoreAPI } from '../types'\n\n// store 全局 API 的注入键符号,用于 provide/inject 传递 GlobalStoreAPI 实例\nexport const STORE_API_SYMBOL = Symbol('ldesign-store-api') as InjectionKey<GlobalStoreAPI>\n// store Pinia 实例的注入键符号,用于 provide/inject 传递 Pinia 实例\nexport const STORE_PINIA_SYMBOL = Symbol('ldesign-store-pinia') as InjectionKey<Pinia>\n// store 运行时实例的注入键符号,用于 provide/inject 传递 StoreRuntime 实例\nexport const STORE_RUNTIME_SYMBOL = Symbol('ldesign-store-runtime') as InjectionKey<StoreRuntime>\n"],"mappings":";;;;;;;;;;;;AAUO,MAAM,mBAAmB,uBAAO,mBAAmB;AAEnD,MAAM,qBAAqB,uBAAO,qBAAqB;AAEvD,MAAM,uBAAuB,uBAAO,uBAAuB"}
1
+ {"version":3,"file":"injection-keys.cjs","names":[],"sources":["../../src/context/injection-keys.ts"],"sourcesContent":["// 从 pinia 导入 Pinia 类型\r\nimport type { Pinia } from 'pinia'\r\n// 从 vue 导入 InjectionKey 类型,用于类型安全的依赖注入\r\nimport type { InjectionKey } from 'vue'\r\n// 导入 StoreRuntime 类型,表示 store 运行时实例\r\nimport type { StoreRuntime } from '../runtime/store-runtime'\r\n// 导入 GlobalStoreAPI 类型,表示全局 store API 接口\r\nimport type { GlobalStoreAPI } from '../types'\r\n\r\n// store 全局 API 的注入键符号,用于 provide/inject 传递 GlobalStoreAPI 实例\r\nexport const STORE_API_SYMBOL = Symbol(\r\n 'ldesign-store-api'\r\n) as InjectionKey<GlobalStoreAPI>\r\n// store Pinia 实例的注入键符号,用于 provide/inject 传递 Pinia 实例\r\nexport const STORE_PINIA_SYMBOL = Symbol(\r\n 'ldesign-store-pinia'\r\n) as InjectionKey<Pinia>\r\n// store 运行时实例的注入键符号,用于 provide/inject 传递 StoreRuntime 实例\r\nexport const STORE_RUNTIME_SYMBOL = Symbol(\r\n 'ldesign-store-runtime'\r\n) as InjectionKey<StoreRuntime>\r\n"],"mappings":";;;;;;;;;;;;AAUO,MAAM,mBAAmB,uBAC9B,mBACF;AAEO,MAAM,qBAAqB,uBAChC,qBACF;AAEO,MAAM,uBAAuB,uBAClC,uBACF"}
@@ -5,4 +5,3 @@ import type { GlobalStoreAPI } from '../types';
5
5
  export declare const STORE_API_SYMBOL: InjectionKey<GlobalStoreAPI>;
6
6
  export declare const STORE_PINIA_SYMBOL: InjectionKey<Pinia>;
7
7
  export declare const STORE_RUNTIME_SYMBOL: InjectionKey<StoreRuntime>;
8
- //# sourceMappingURL=injection-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injection-keys.d.cts","sourceRoot":"","sources":["../../src/context/injection-keys.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAA;AAElC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,KAAK,CAAA;AAEvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAG9C,eAAO,MAAM,gBAAgB,EAExB,YAAY,CAAC,cAAc,CAAC,CAAA;AAEjC,eAAO,MAAM,kBAAkB,EAE1B,YAAY,CAAC,KAAK,CAAC,CAAA;AAExB,eAAO,MAAM,oBAAoB,EAE5B,YAAY,CAAC,YAAY,CAAC,CAAA"}