@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":"useOptimistic.js","names":[],"sources":["../../src/composables/useOptimistic.ts"],"sourcesContent":["/**\n * useOptimistic - 乐观更新 Hook\n *\n * 提供乐观更新模式:先立即更新 UI,再异步确认,失败时自动回滚\n * @module composables/useOptimistic\n */\n\nimport type {Ref} from 'vue';\nimport { deepClone } from '@composy/store-core'\nimport { ref } from 'vue'\n\n/**\n * 乐观更新操作记录\n */\nexport interface OptimisticEntry<T> {\n /** 操作 ID */\n id: string\n /** 提交前的快照 */\n snapshot: T\n /** 操作状态 */\n status: 'pending' | 'confirmed' | 'rolled-back'\n /** 时间戳 */\n timestamp: number\n}\n\n/**\n * useOptimistic 返回值\n */\nexport interface UseOptimisticReturn<T> {\n /** 响应式状态 */\n state: Ref<T>\n /** 是否有挂起的操作 */\n isPending: Ref<boolean>\n /** 挂起的操作记录 */\n pending: Ref<OptimisticEntry<T>[]>\n /** 最近一次操作是否已回滚 */\n isRolledBack: Ref<boolean>\n /** 执行乐观更新: 先 apply 新值,然后 confirm 异步确认 */\n mutate: (\n optimisticValue: T | ((prev: T) => T),\n confirm: () => Promise<void>,\n ) => Promise<void>\n}\n\n/**\n * 生成唯一 ID\n */\nfunction uid(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * 乐观更新 Hook\n *\n * 先立即更新 UI(乐观),再执行异步确认操作。\n * 若确认失败则自动回滚到更新前的状态。\n *\n * @template T - 状态类型\n * @param initialState - 初始状态\n * @returns 乐观更新 API\n *\n * @example\n * ```typescript\n * const { state, isPending, isRolledBack, mutate } = useOptimistic({\n * items: ['Apple', 'Banana'],\n * })\n *\n * // 乐观添加\n * await mutate(\n * (prev) => ({ ...prev, items: [...prev.items, 'Cherry'] }),\n * async () => {\n * await fetch('/api/items', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'Cherry' }),\n * })\n * }\n * )\n * // 成功: state 保持新值\n * // 失败: state 自动回滚到添加之前\n * ```\n */\nexport function useOptimistic<T>(initialState: T): UseOptimisticReturn<T> {\n const state = ref<T>(deepClone(initialState)) as Ref<T>\n const isPending = ref(false)\n const isRolledBack = ref(false)\n const pending = ref<OptimisticEntry<T>[]>([]) as Ref<OptimisticEntry<T>[]>\n\n async function mutate(\n optimisticValue: T | ((prev: T) => T),\n confirm: () => Promise<void>,\n ): Promise<void> {\n // 保存快照\n const snapshot: T = deepClone(state.value)\n const entryId = uid()\n const entry: OptimisticEntry<T> = {\n id: entryId,\n snapshot,\n status: 'pending',\n timestamp: Date.now(),\n }\n\n pending.value = [...pending.value, entry]\n isPending.value = true\n isRolledBack.value = false\n\n // 立即应用乐观值\n if (typeof optimisticValue === 'function') {\n state.value = (optimisticValue as (prev: T) => T)(state.value)\n }\n else {\n state.value = optimisticValue\n }\n\n try {\n // 异步确认\n await confirm()\n\n // 确认成功\n pending.value = pending.value.map(e =>\n e.id === entryId ? { ...e, status: 'confirmed' as const } : e,\n )\n }\n catch {\n // 确认失败 - 回滚\n state.value = snapshot\n isRolledBack.value = true\n\n pending.value = pending.value.map(e =>\n e.id === entryId ? { ...e, status: 'rolled-back' as const } : e,\n )\n }\n finally {\n // 清除已完成的条目\n pending.value = pending.value.filter(e => e.status === 'pending')\n isPending.value = pending.value.length > 0\n }\n }\n\n return {\n state,\n isPending,\n pending,\n isRolledBack,\n mutate,\n }\n}\n"],"mappings":";;;;;;;;;;;;AA+CA,SAAS,MAAc;CACrB,OAAO,GAAG,KAAK,IAAI,EAAC,GAAI,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,CAAC;AAC/D;AAgCO,SAAS,cAAiB,cAAyC;CACxE,MAAM,QAAQ,IAAO,UAAU,YAAY,CAAC;CAC5C,MAAM,YAAY,IAAI,KAAK;CAC3B,MAAM,eAAe,IAAI,KAAK;CAC9B,MAAM,UAAU,IAA0B,CAAC,CAAC;CAE5C,eAAe,OACb,iBACA,SACe;EAEf,MAAM,WAAc,UAAU,MAAM,KAAK;EACzC,MAAM,UAAU,IAAI;EACpB,MAAM,QAA4B;GAChC,IAAI;GACJ;GACA,QAAQ;GACR,WAAW,KAAK,IAAI;EACtB;EAEA,QAAQ,QAAQ,CAAC,GAAG,QAAQ,OAAO,KAAK;EACxC,UAAU,QAAQ;EAClB,aAAa,QAAQ;EAGrB,IAAI,OAAO,oBAAoB,YAC7B,MAAM,QAAS,gBAAmC,MAAM,KAAK;OAG7D,MAAM,QAAQ;EAGhB,IAAI;GAEF,MAAM,QAAQ;GAGd,QAAQ,QAAQ,QAAQ,MAAM,KAAI,MAChC,EAAE,OAAO,UAAU;IAAE,GAAG;IAAG,QAAQ;GAAqB,IAAI,CAC9D;EACF,QACM;GAEJ,MAAM,QAAQ;GACd,aAAa,QAAQ;GAErB,QAAQ,QAAQ,QAAQ,MAAM,KAAI,MAChC,EAAE,OAAO,UAAU;IAAE,GAAG;IAAG,QAAQ;GAAuB,IAAI,CAChE;EACF,UACA;GAEE,QAAQ,QAAQ,QAAQ,MAAM,QAAO,MAAK,EAAE,WAAW,SAAS;GAChE,UAAU,QAAQ,QAAQ,MAAM,SAAS;EAC3C;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useOptimistic.js","names":[],"sources":["../../src/composables/useOptimistic.ts"],"sourcesContent":["/** Optimistic state helper with an operation log for overlapping mutations. */\n\nimport type { Ref } from 'vue'\nimport { deepClone } from '@composy/store-core'\nimport { ref } from 'vue'\n\nexport interface OptimisticEntry<T> {\n id: string\n snapshot: T\n status: 'pending' | 'confirmed' | 'rolled-back'\n timestamp: number\n}\n\nexport interface UseOptimisticReturn<T> {\n state: Ref<T>\n isPending: Ref<boolean>\n pending: Ref<OptimisticEntry<T>[]>\n isRolledBack: Ref<boolean>\n mutate: (\n optimisticValue: T | ((prev: T) => T),\n confirm: () => Promise<void>\n ) => Promise<void>\n}\n\ninterface Operation<T> extends OptimisticEntry<T> {\n apply: (previous: T) => T\n}\n\nfunction uid(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\nexport function useOptimistic<T>(initialState: T): UseOptimisticReturn<T> {\n const state = ref<T>(deepClone(initialState)) as Ref<T>\n let committedState = deepClone(initialState)\n const operations: Operation<T>[] = []\n const isPending = ref(false)\n const isRolledBack = ref(false)\n const pending = ref<OptimisticEntry<T>[]>([]) as Ref<OptimisticEntry<T>[]>\n\n const refresh = (): void => {\n let nextState = deepClone(committedState)\n operations.forEach(operation => {\n if (operation.status !== 'rolled-back') {\n nextState = operation.apply(nextState)\n }\n })\n state.value = deepClone(nextState)\n pending.value = operations\n .filter(operation => operation.status === 'pending')\n .map(operation => ({\n id: operation.id,\n snapshot: deepClone(operation.snapshot),\n status: operation.status,\n timestamp: operation.timestamp,\n }))\n isPending.value = pending.value.length > 0\n }\n\n const reconcile = (): void => {\n while (operations[0] && operations[0].status !== 'pending') {\n const operation = operations.shift()\n if (!operation || operation.status === 'rolled-back') continue\n committedState = operation.apply(deepClone(committedState))\n }\n refresh()\n }\n\n async function mutate(\n optimisticValue: T | ((prev: T) => T),\n confirm: () => Promise<void>\n ): Promise<void> {\n const snapshot = deepClone(state.value)\n const apply =\n typeof optimisticValue === 'function'\n ? (previous: T): T =>\n deepClone((optimisticValue as (prev: T) => T)(deepClone(previous)))\n : (): T => deepClone(optimisticValue)\n const operation: Operation<T> = {\n id: uid(),\n snapshot,\n status: 'pending',\n timestamp: Date.now(),\n apply,\n }\n\n operations.push(operation)\n isRolledBack.value = false\n refresh()\n\n try {\n await confirm()\n operation.status = 'confirmed'\n } catch {\n operation.status = 'rolled-back'\n isRolledBack.value = true\n } finally {\n reconcile()\n }\n }\n\n return {\n state,\n isPending,\n pending,\n isRolledBack,\n mutate,\n }\n}\n"],"mappings":";;;;;;;;;;;;AA4BA,SAAS,MAAc;CACrB,OAAO,GAAG,KAAK,IAAI,EAAC,GAAI,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,CAAC;AAC/D;AAEO,SAAS,cAAiB,cAAyC;CACxE,MAAM,QAAQ,IAAO,UAAU,YAAY,CAAC;CAC5C,IAAI,iBAAiB,UAAU,YAAY;CAC3C,MAAM,aAA6B,CAAC;CACpC,MAAM,YAAY,IAAI,KAAK;CAC3B,MAAM,eAAe,IAAI,KAAK;CAC9B,MAAM,UAAU,IAA0B,CAAC,CAAC;CAE5C,MAAM,gBAAsB;EAC1B,IAAI,YAAY,UAAU,cAAc;EACxC,WAAW,SAAQ,cAAa;GAC9B,IAAI,UAAU,WAAW,eACvB,YAAY,UAAU,MAAM,SAAS;EAEzC,CAAC;EACD,MAAM,QAAQ,UAAU,SAAS;EACjC,QAAQ,QAAQ,WACb,QAAO,cAAa,UAAU,WAAW,SAAS,CAAA,CAClD,KAAI,eAAc;GACjB,IAAI,UAAU;GACd,UAAU,UAAU,UAAU,QAAQ;GACtC,QAAQ,UAAU;GAClB,WAAW,UAAU;EACvB,EAAE;EACJ,UAAU,QAAQ,QAAQ,MAAM,SAAS;CAC3C;CAEA,MAAM,kBAAwB;EAC5B,OAAO,WAAW,MAAM,WAAW,EAAC,CAAE,WAAW,WAAW;GAC1D,MAAM,YAAY,WAAW,MAAM;GACnC,IAAI,CAAC,aAAa,UAAU,WAAW,eAAe;GACtD,iBAAiB,UAAU,MAAM,UAAU,cAAc,CAAC;EAC5D;EACA,QAAQ;CACV;CAEA,eAAe,OACb,iBACA,SACe;EACf,MAAM,WAAW,UAAU,MAAM,KAAK;EACtC,MAAM,QACJ,OAAO,oBAAoB,cACtB,aACC,UAAW,gBAAmC,UAAU,QAAQ,CAAC,CAAC,UAC3D,UAAU,eAAe;EACxC,MAAM,YAA0B;GAC9B,IAAI,IAAI;GACR;GACA,QAAQ;GACR,WAAW,KAAK,IAAI;GACpB;EACF;EAEA,WAAW,KAAK,SAAS;EACzB,aAAa,QAAQ;EACrB,QAAQ;EAER,IAAI;GACF,MAAM,QAAQ;GACd,UAAU,SAAS;EACrB,QAAQ;GACN,UAAU,SAAS;GACnB,aAAa,QAAQ;EACvB,UAAE;GACA,UAAU;EACZ;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -65,4 +65,3 @@ export declare function usePersistedRef<T>(key: string, defaultValue: T, storage
65
65
  * @returns 持久化的 ref
66
66
  */
67
67
  export declare function usePersistedRefWithTTL<T>(key: string, defaultValue: T, ttl: number): Ref<T>;
68
- //# sourceMappingURL=usePersist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePersist.d.ts","sourceRoot":"","sources":["../../src/composables/usePersist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAanE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAAC,CAAC,CAAC,CA8E7E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,cAAc,GAAG,gBAAiC,GAC1D,GAAG,CAAC,CAAC,CAAC,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,GAAG,EAAE,MAAM,GACV,GAAG,CAAC,CAAC,CAAC,CA8BR"}
1
+ {"version":3,"file":"usePersist.d.ts","sourceRoot":"","sources":["../../src/composables/usePersist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAiBnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAAC,CAAC,CAAC,CA8E7E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,cAAc,GAAG,gBAAiC,GAC1D,GAAG,CAAC,CAAC,CAAC,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,GAAG,EAAE,MAAM,GACV,GAAG,CAAC,CAAC,CAAC,CA0CR"}
@@ -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
  * ***********************************
@@ -11,10 +11,14 @@ import { onMounted, ref, watch } from "vue";
11
11
  //#region src/composables/usePersist.ts
12
12
  function getStorage(type) {
13
13
  if (typeof window === "undefined") return null;
14
- return type === "localStorage" ? localStorage : sessionStorage;
14
+ try {
15
+ return type === "localStorage" ? window.localStorage : window.sessionStorage;
16
+ } catch {
17
+ return null;
18
+ }
15
19
  }
16
20
  function usePersist(options) {
17
- const { key, storage: storageType = "localStorage", defaultValue, serialize = JSON.stringify, deserialize = JSON.parse } = options;
21
+ const { key, storage: storageType = "localStorage", defaultValue, serialize = JSON.stringify, deserialize = JSON.parse, restoreOnMounted = true } = options;
18
22
  const storage = getStorage(storageType);
19
23
  const value = ref(defaultValue);
20
24
  const restore = () => {
@@ -43,7 +47,7 @@ function usePersist(options) {
43
47
  console.error(`[usePersist] \u6E05\u9664\u5931\u8D25 (${key}):`, error);
44
48
  }
45
49
  };
46
- onMounted(() => {
50
+ if (restoreOnMounted) onMounted(() => {
47
51
  restore();
48
52
  });
49
53
  watch(value, () => {
@@ -65,12 +69,13 @@ function usePersistedRef(key, defaultValue, storage = "localStorage") {
65
69
  return value;
66
70
  }
67
71
  function usePersistedRefWithTTL(key, defaultValue, ttl) {
68
- const { value } = usePersist({
72
+ const persisted = usePersist({
69
73
  key,
70
74
  defaultValue: {
71
75
  value: defaultValue,
72
76
  expiry: 0
73
77
  },
78
+ restoreOnMounted: false,
74
79
  serialize: (data) => JSON.stringify(data),
75
80
  deserialize: (str) => {
76
81
  const data = JSON.parse(str);
@@ -81,13 +86,17 @@ function usePersistedRefWithTTL(key, defaultValue, ttl) {
81
86
  return data;
82
87
  }
83
88
  });
84
- const wrappedRef = ref(value.value.value);
89
+ persisted.restore();
90
+ const wrappedRef = ref(persisted.value.value.value);
85
91
  watch(wrappedRef, (newValue) => {
86
- value.value = {
92
+ persisted.value.value = {
87
93
  value: newValue,
88
94
  expiry: Date.now() + ttl
89
95
  };
90
96
  });
97
+ watch(persisted.value, (stored) => {
98
+ if (!Object.is(wrappedRef.value, stored.value)) wrappedRef.value = stored.value;
99
+ }, { deep: true });
91
100
  return wrappedRef;
92
101
  }
93
102
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"usePersist.js","names":[],"sources":["../../src/composables/usePersist.ts"],"sourcesContent":["/**\n * usePersist Composable\n *\n * 提供状态持久化功能\n * @module composables/usePersist\n */\n\nimport type { Ref } from 'vue'\nimport type { UsePersistOptions, UsePersistReturn } from '../types'\nimport { onMounted, ref, watch } from 'vue'\n\n/**\n * 获取存储对象\n */\nfunction getStorage(type: 'localStorage' | 'sessionStorage'): Storage | null {\n if (typeof window === 'undefined') {\n return null\n }\n return type === 'localStorage' ? localStorage : sessionStorage\n}\n\n/**\n * 状态持久化 Hook\n *\n * 将状态自动保存到 localStorage 或 sessionStorage\n *\n * @template T - 值类型\n * @param options - 持久化选项\n * @returns 持久化操作对象\n *\n * @example\n * ```typescript\n * const { value, save, restore, clear } = usePersist<User>({\n * key: 'user-info',\n * storage: 'localStorage',\n * defaultValue: { name: '', age: 0 }\n * })\n *\n * // 更新值会自动保存\n * value.value = { name: 'John', age: 25 }\n *\n * // 手动保存\n * save()\n *\n * // 从存储恢复\n * restore()\n *\n * // 清除存储\n * clear()\n * ```\n */\nexport function usePersist<T>(options: UsePersistOptions): UsePersistReturn<T> {\n const {\n key,\n storage: storageType = 'localStorage',\n defaultValue,\n serialize = JSON.stringify,\n deserialize = JSON.parse,\n } = options\n\n const storage = getStorage(storageType)\n const value = ref<T>(defaultValue as T) as Ref<T>\n\n /**\n * 从存储恢复\n */\n const restore = (): void => {\n if (!storage) return\n\n try {\n const stored = storage.getItem(key)\n if (stored !== null) {\n value.value = deserialize(stored) as T\n }\n }\n catch (error) {\n console.error(`[usePersist] 恢复失败 (${key}):`, error)\n }\n }\n\n /**\n * 保存到存储\n */\n const save = (): void => {\n if (!storage) return\n\n try {\n storage.setItem(key, serialize(value.value))\n }\n catch (error) {\n console.error(`[usePersist] 保存失败 (${key}):`, error)\n }\n }\n\n /**\n * 清除存储\n */\n const clear = (): void => {\n if (!storage) return\n\n try {\n storage.removeItem(key)\n value.value = defaultValue as T\n }\n catch (error) {\n console.error(`[usePersist] 清除失败 (${key}):`, error)\n }\n }\n\n // 组件挂载时恢复\n onMounted(() => {\n restore()\n })\n\n // 监听变化自动保存\n watch(\n value,\n () => {\n save()\n },\n { deep: true },\n )\n\n return {\n value,\n save,\n restore,\n clear,\n }\n}\n\n/**\n * 创建持久化的 ref\n *\n * @template T - 值类型\n * @param key - 存储键名\n * @param defaultValue - 默认值\n * @param storage - 存储类型\n * @returns 持久化的 ref\n *\n * @example\n * ```typescript\n * const count = usePersistedRef('count', 0)\n *\n * // 更新会自动保存\n * count.value++\n * ```\n */\nexport function usePersistedRef<T>(\n key: string,\n defaultValue: T,\n storage: 'localStorage' | 'sessionStorage' = 'localStorage',\n): Ref<T> {\n const { value } = usePersist<T>({\n key,\n storage,\n defaultValue,\n })\n return value\n}\n\n/**\n * 创建带过期时间的持久化 ref\n *\n * @template T - 值类型\n * @param key - 存储键名\n * @param defaultValue - 默认值\n * @param ttl - 过期时间(毫秒)\n * @returns 持久化的 ref\n */\nexport function usePersistedRefWithTTL<T>(\n key: string,\n defaultValue: T,\n ttl: number,\n): Ref<T> {\n interface StoredData {\n value: T\n expiry: number\n }\n\n const { value } = usePersist<StoredData>({\n key,\n defaultValue: { value: defaultValue, expiry: 0 },\n serialize: (data: unknown) => JSON.stringify(data),\n deserialize: (str: string) => {\n const data = JSON.parse(str) as StoredData\n if (data.expiry && Date.now() > data.expiry) {\n return { value: defaultValue, expiry: 0 }\n }\n return data\n },\n })\n\n // 包装 ref 以自动设置过期时间\n const wrappedRef = ref(value.value.value) as Ref<T>\n\n watch(wrappedRef, (newValue) => {\n value.value = {\n value: newValue,\n expiry: Date.now() + ttl,\n }\n })\n\n return wrappedRef\n}\n\n"],"mappings":";;;;;;;;;;;AAcA,SAAS,WAAW,MAAyD;CAC3E,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,OAAO,SAAS,iBAAiB,eAAe;AAClD;AAgCO,SAAS,WAAc,SAAiD;CAC7E,MAAM,EACJ,KACA,SAAS,cAAc,gBACvB,cACA,YAAY,KAAK,WACjB,cAAc,KAAK,UACjB;CAEJ,MAAM,UAAU,WAAW,WAAW;CACtC,MAAM,QAAQ,IAAO,YAAiB;CAKtC,MAAM,gBAAsB;EAC1B,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,MAAM,SAAS,QAAQ,QAAQ,GAAG;GAClC,IAAI,WAAW,MACb,MAAM,QAAQ,YAAY,MAAM;EAEpC,SACO,OAAO;GACZ,QAAQ,MAAM,0CAAsB,IAAG,KAAM,KAAK;EACpD;CACF;CAKA,MAAM,aAAmB;EACvB,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,QAAQ,QAAQ,KAAK,UAAU,MAAM,KAAK,CAAC;EAC7C,SACO,OAAO;GACZ,QAAQ,MAAM,0CAAsB,IAAG,KAAM,KAAK;EACpD;CACF;CAKA,MAAM,cAAoB;EACxB,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,QAAQ,WAAW,GAAG;GACtB,MAAM,QAAQ;EAChB,SACO,OAAO;GACZ,QAAQ,MAAM,0CAAsB,IAAG,KAAM,KAAK;EACpD;CACF;CAGA,gBAAgB;EACd,QAAQ;CACV,CAAC;CAGD,MACE,aACM;EACJ,KAAK;CACP,GACA,EAAE,MAAM,KAAK,CACf;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF;AAmBO,SAAS,gBACd,KACA,cACA,UAA6C,gBACrC;CACR,MAAM,EAAE,UAAU,WAAc;EAC9B;EACA;EACA;CACF,CAAC;CACD,OAAO;AACT;AAWO,SAAS,uBACd,KACA,cACA,KACQ;CAMR,MAAM,EAAE,UAAU,WAAuB;EACvC;EACA,cAAc;GAAE,OAAO;GAAc,QAAQ;EAAE;EAC/C,YAAY,SAAkB,KAAK,UAAU,IAAI;EACjD,cAAc,QAAgB;GAC5B,MAAM,OAAO,KAAK,MAAM,GAAG;GAC3B,IAAI,KAAK,UAAU,KAAK,IAAI,IAAI,KAAK,QACnC,OAAO;IAAE,OAAO;IAAc,QAAQ;GAAE;GAE1C,OAAO;EACT;CACF,CAAC;CAGD,MAAM,aAAa,IAAI,MAAM,MAAM,KAAK;CAExC,MAAM,aAAa,aAAa;EAC9B,MAAM,QAAQ;GACZ,OAAO;GACP,QAAQ,KAAK,IAAI,IAAI;EACvB;CACF,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"usePersist.js","names":[],"sources":["../../src/composables/usePersist.ts"],"sourcesContent":["/**\r\n * usePersist Composable\r\n *\r\n * 提供状态持久化功能\r\n * @module composables/usePersist\r\n */\r\n\r\nimport type { Ref } from 'vue'\r\nimport type { UsePersistOptions, UsePersistReturn } from '../types'\r\nimport { onMounted, ref, watch } from 'vue'\r\n\r\n/**\r\n * 获取存储对象\r\n */\r\nfunction getStorage(type: 'localStorage' | 'sessionStorage'): Storage | null {\r\n if (typeof window === 'undefined') {\r\n return null\r\n }\r\n try {\r\n return type === 'localStorage' ? window.localStorage : window.sessionStorage\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\n/**\r\n * 状态持久化 Hook\r\n *\r\n * 将状态自动保存到 localStorage 或 sessionStorage\r\n *\r\n * @template T - 值类型\r\n * @param options - 持久化选项\r\n * @returns 持久化操作对象\r\n *\r\n * @example\r\n * ```typescript\r\n * const { value, save, restore, clear } = usePersist<User>({\r\n * key: 'user-info',\r\n * storage: 'localStorage',\r\n * defaultValue: { name: '', age: 0 }\r\n * })\r\n *\r\n * // 更新值会自动保存\r\n * value.value = { name: 'John', age: 25 }\r\n *\r\n * // 手动保存\r\n * save()\r\n *\r\n * // 从存储恢复\r\n * restore()\r\n *\r\n * // 清除存储\r\n * clear()\r\n * ```\r\n */\r\nexport function usePersist<T>(options: UsePersistOptions): UsePersistReturn<T> {\r\n const {\r\n key,\r\n storage: storageType = 'localStorage',\r\n defaultValue,\r\n serialize = JSON.stringify,\r\n deserialize = JSON.parse,\r\n restoreOnMounted = true,\r\n } = options\r\n\r\n const storage = getStorage(storageType)\r\n const value = ref<T>(defaultValue as T) as Ref<T>\r\n\r\n /**\r\n * 从存储恢复\r\n */\r\n const restore = (): void => {\r\n if (!storage) return\r\n\r\n try {\r\n const stored = storage.getItem(key)\r\n if (stored !== null) {\r\n value.value = deserialize(stored) as T\r\n }\r\n } catch (error) {\r\n console.error(`[usePersist] 恢复失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n /**\r\n * 保存到存储\r\n */\r\n const save = (): void => {\r\n if (!storage) return\r\n\r\n try {\r\n storage.setItem(key, serialize(value.value))\r\n } catch (error) {\r\n console.error(`[usePersist] 保存失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n /**\r\n * 清除存储\r\n */\r\n const clear = (): void => {\r\n if (!storage) return\r\n\r\n try {\r\n storage.removeItem(key)\r\n value.value = defaultValue as T\r\n } catch (error) {\r\n console.error(`[usePersist] 清除失败 (${key}):`, error)\r\n }\r\n }\r\n\r\n // 组件挂载时恢复\r\n if (restoreOnMounted) {\r\n onMounted(() => {\r\n restore()\r\n })\r\n }\r\n\r\n // 监听变化自动保存\r\n watch(\r\n value,\r\n () => {\r\n save()\r\n },\r\n { deep: true }\r\n )\r\n\r\n return {\r\n value,\r\n save,\r\n restore,\r\n clear,\r\n }\r\n}\r\n\r\n/**\r\n * 创建持久化的 ref\r\n *\r\n * @template T - 值类型\r\n * @param key - 存储键名\r\n * @param defaultValue - 默认值\r\n * @param storage - 存储类型\r\n * @returns 持久化的 ref\r\n *\r\n * @example\r\n * ```typescript\r\n * const count = usePersistedRef('count', 0)\r\n *\r\n * // 更新会自动保存\r\n * count.value++\r\n * ```\r\n */\r\nexport function usePersistedRef<T>(\r\n key: string,\r\n defaultValue: T,\r\n storage: 'localStorage' | 'sessionStorage' = 'localStorage'\r\n): Ref<T> {\r\n const { value } = usePersist<T>({\r\n key,\r\n storage,\r\n defaultValue,\r\n })\r\n return value\r\n}\r\n\r\n/**\r\n * 创建带过期时间的持久化 ref\r\n *\r\n * @template T - 值类型\r\n * @param key - 存储键名\r\n * @param defaultValue - 默认值\r\n * @param ttl - 过期时间(毫秒)\r\n * @returns 持久化的 ref\r\n */\r\nexport function usePersistedRefWithTTL<T>(\r\n key: string,\r\n defaultValue: T,\r\n ttl: number\r\n): Ref<T> {\r\n interface StoredData {\r\n value: T\r\n expiry: number\r\n }\r\n\r\n const persisted = usePersist<StoredData>({\r\n key,\r\n defaultValue: { value: defaultValue, expiry: 0 },\r\n restoreOnMounted: false,\r\n serialize: (data: unknown) => JSON.stringify(data),\r\n deserialize: (str: string) => {\r\n const data = JSON.parse(str) as StoredData\r\n if (data.expiry && Date.now() > data.expiry) {\r\n return { value: defaultValue, expiry: 0 }\r\n }\r\n return data\r\n },\r\n })\r\n\r\n // 包装 ref 以自动设置过期时间\r\n persisted.restore()\r\n const wrappedRef = ref(persisted.value.value.value) as Ref<T>\r\n\r\n watch(wrappedRef, newValue => {\r\n persisted.value.value = {\r\n value: newValue,\r\n expiry: Date.now() + ttl,\r\n }\r\n })\r\n\r\n watch(\r\n persisted.value,\r\n stored => {\r\n if (!Object.is(wrappedRef.value, stored.value)) {\r\n wrappedRef.value = stored.value\r\n }\r\n },\r\n { deep: true }\r\n )\r\n\r\n return wrappedRef\r\n}\r\n"],"mappings":";;;;;;;;;;;AAcA,SAAS,WAAW,MAAyD;CAC3E,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,IAAI;EACF,OAAO,SAAS,iBAAiB,OAAO,eAAe,OAAO;CAChE,QAAQ;EACN,OAAO;CACT;AACF;AAgCO,SAAS,WAAc,SAAiD;CAC7E,MAAM,EACJ,KACA,SAAS,cAAc,gBACvB,cACA,YAAY,KAAK,WACjB,cAAc,KAAK,OACnB,mBAAmB,SACjB;CAEJ,MAAM,UAAU,WAAW,WAAW;CACtC,MAAM,QAAQ,IAAO,YAAiB;CAKtC,MAAM,gBAAsB;EAC1B,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,MAAM,SAAS,QAAQ,QAAQ,GAAG;GAClC,IAAI,WAAW,MACb,MAAM,QAAQ,YAAY,MAAM;EAEpC,SAAS,OAAO;GACd,QAAQ,MAAM,0CAAsB,IAAG,KAAM,KAAK;EACpD;CACF;CAKA,MAAM,aAAmB;EACvB,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,QAAQ,QAAQ,KAAK,UAAU,MAAM,KAAK,CAAC;EAC7C,SAAS,OAAO;GACd,QAAQ,MAAM,0CAAsB,IAAG,KAAM,KAAK;EACpD;CACF;CAKA,MAAM,cAAoB;EACxB,IAAI,CAAC,SAAS;EAEd,IAAI;GACF,QAAQ,WAAW,GAAG;GACtB,MAAM,QAAQ;EAChB,SAAS,OAAO;GACd,QAAQ,MAAM,0CAAsB,IAAG,KAAM,KAAK;EACpD;CACF;CAGA,IAAI,kBACF,gBAAgB;EACd,QAAQ;CACV,CAAC;CAIH,MACE,aACM;EACJ,KAAK;CACP,GACA,EAAE,MAAM,KAAK,CACf;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF;AAmBO,SAAS,gBACd,KACA,cACA,UAA6C,gBACrC;CACR,MAAM,EAAE,UAAU,WAAc;EAC9B;EACA;EACA;CACF,CAAC;CACD,OAAO;AACT;AAWO,SAAS,uBACd,KACA,cACA,KACQ;CAMR,MAAM,YAAY,WAAuB;EACvC;EACA,cAAc;GAAE,OAAO;GAAc,QAAQ;EAAE;EAC/C,kBAAkB;EAClB,YAAY,SAAkB,KAAK,UAAU,IAAI;EACjD,cAAc,QAAgB;GAC5B,MAAM,OAAO,KAAK,MAAM,GAAG;GAC3B,IAAI,KAAK,UAAU,KAAK,IAAI,IAAI,KAAK,QACnC,OAAO;IAAE,OAAO;IAAc,QAAQ;GAAE;GAE1C,OAAO;EACT;CACF,CAAC;CAGD,UAAU,QAAQ;CAClB,MAAM,aAAa,IAAI,UAAU,MAAM,MAAM,KAAK;CAElD,MAAM,aAAY,aAAY;EAC5B,UAAU,MAAM,QAAQ;GACtB,OAAO;GACP,QAAQ,KAAK,IAAI,IAAI;EACvB;CACF,CAAC;CAED,MACE,UAAU,QACV,WAAU;EACR,IAAI,CAAC,OAAO,GAAG,WAAW,OAAO,OAAO,KAAK,GAC3C,WAAW,QAAQ,OAAO;CAE9B,GACA,EAAE,MAAM,KAAK,CACf;CAEA,OAAO;AACT"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"usePersistedState.d.ts","sourceRoot":"","sources":["../../src/composables/usePersistedState.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,KAAK,CAAC;AAM7B;;;;;;;;;;;;;;;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;AAoDD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAChC,oBAAoB,CAAC,CAAC,CAAC,CAgOzB"}
1
+ {"version":3,"file":"usePersistedState.d.ts","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
  * ***********************************
@@ -29,6 +29,7 @@ function usePersistedState(options) {
29
29
  const storage = getStorage(storageType);
30
30
  const createDefaultValue = () => deepClone(defaultValue);
31
31
  const state = ref(createDefaultValue());
32
+ let skipNextSave = false;
32
33
  const save = () => {
33
34
  if (!storage) return;
34
35
  try {
@@ -42,6 +43,7 @@ function usePersistedState(options) {
42
43
  console.error(`[usePersistedState] \u4FDD\u5B58\u5931\u8D25 (${key}):`, error);
43
44
  }
44
45
  };
46
+ const debouncedSave = debounceDelay > 0 ? debounce(save, debounceDelay) : null;
45
47
  const clear = () => {
46
48
  if (!storage) return;
47
49
  try {
@@ -69,6 +71,8 @@ function usePersistedState(options) {
69
71
  }
70
72
  };
71
73
  const reset = () => {
74
+ debouncedSave?.cancel();
75
+ skipNextSave = true;
72
76
  state.value = createDefaultValue();
73
77
  clear();
74
78
  };
@@ -83,9 +87,14 @@ function usePersistedState(options) {
83
87
  return false;
84
88
  }
85
89
  };
86
- const debouncedSave = debounceDelay > 0 ? debounce(save, debounceDelay) : null;
87
90
  const handleStorageEvent = (event) => {
88
- if (event.key !== key || !event.newValue) return;
91
+ if (event.key !== key) return;
92
+ if (!event.newValue) {
93
+ debouncedSave?.cancel();
94
+ skipNextSave = true;
95
+ state.value = createDefaultValue();
96
+ return;
97
+ }
89
98
  try {
90
99
  const item = JSON.parse(event.newValue);
91
100
  if (item.expiresAt && Date.now() > item.expiresAt) return;
@@ -96,6 +105,10 @@ function usePersistedState(options) {
96
105
  };
97
106
  restore();
98
107
  watch(state, (newValue, oldValue) => {
108
+ if (skipNextSave) {
109
+ skipNextSave = false;
110
+ return;
111
+ }
99
112
  if (onBeforeChange?.(newValue, oldValue) === false) return;
100
113
  if (debouncedSave) debouncedSave();
101
114
  else save();
@@ -1 +1 @@
1
- {"version":3,"file":"usePersistedState.js","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.js","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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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"useSharedState.d.ts","sourceRoot":"","sources":["../../src/composables/useSharedState.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,KAAK,CAAC;AAI7B;;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,CAgHzB"}
1
+ {"version":3,"file":"useSharedState.d.ts","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,13 +2,13 @@
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
  * ***********************************
9
9
  */
10
10
  import { onUnmounted, ref, watch } from "vue";
11
- import { deepClone } from "@composy/store-core";
11
+ import { deepClone, deepEqual } from "@composy/store-core";
12
12
  //#region src/composables/useSharedState.ts
13
13
  function useSharedState(channelName, initialState, options = {}) {
14
14
  const { immediate = false, deep = true } = options;
@@ -16,7 +16,11 @@ function useSharedState(channelName, initialState, options = {}) {
16
16
  const peerCount = ref(0);
17
17
  const lastSyncAt = ref(null);
18
18
  let channel = null;
19
- let isReceiving = false;
19
+ const originId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
20
+ let revision = 0;
21
+ const latestRemoteRevision = /* @__PURE__ */ new Map();
22
+ let pendingRemoteState;
23
+ let hasPendingRemoteState = false;
20
24
  if (typeof BroadcastChannel === "undefined") {
21
25
  console.warn("[useSharedState] BroadcastChannel not supported in this environment");
22
26
  return {
@@ -45,7 +49,9 @@ function useSharedState(channelName, initialState, options = {}) {
45
49
  channel.postMessage({
46
50
  type: "state-update",
47
51
  payload: deepClone(state.value),
48
- timestamp: Date.now()
52
+ timestamp: Date.now(),
53
+ origin: originId,
54
+ revision: ++revision
49
55
  });
50
56
  } catch (e) {
51
57
  console.warn("[useSharedState] broadcast error:", e);
@@ -55,15 +61,26 @@ function useSharedState(channelName, initialState, options = {}) {
55
61
  const msg = event.data;
56
62
  if (!msg || typeof msg !== "object") return;
57
63
  if (msg.type === "state-update") {
58
- isReceiving = true;
59
- state.value = msg.payload;
64
+ if (msg.origin === originId) return;
65
+ const remoteRevision = typeof msg.revision === "number" ? msg.revision : 0;
66
+ if (remoteRevision <= (latestRemoteRevision.get(String(msg.origin)) ?? -1)) return;
67
+ latestRemoteRevision.set(String(msg.origin), remoteRevision);
68
+ pendingRemoteState = deepClone(msg.payload);
69
+ hasPendingRemoteState = true;
70
+ state.value = pendingRemoteState;
60
71
  lastSyncAt.value = msg.timestamp || Date.now();
61
- isReceiving = false;
62
72
  } else if (msg.type === "ping") channel?.postMessage({ type: "pong" });
63
73
  else if (msg.type === "pong") peerCount.value++;
64
74
  };
65
75
  const stopWatch = watch(state, () => {
66
- if (!isReceiving) broadcast();
76
+ if (hasPendingRemoteState) {
77
+ if (deepEqual(state.value, pendingRemoteState)) {
78
+ hasPendingRemoteState = false;
79
+ return;
80
+ }
81
+ hasPendingRemoteState = false;
82
+ }
83
+ broadcast();
67
84
  }, { deep });
68
85
  function close() {
69
86
  stopWatch();
@@ -1 +1 @@
1
- {"version":3,"file":"useSharedState.js","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,QAAQ,IAAO,UAAU,YAAY,CAAC;CAC5C,MAAM,YAAY,IAAI,CAAC;CACvB,MAAM,aAAa,IAAmB,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,SAAS,UAAU,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,YAAY,MAChB,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,kBAAkB;EAChB,MAAM;CACR,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useSharedState.js","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,QAAQ,IAAO,UAAU,YAAY,CAAC;CAC5C,MAAM,YAAY,IAAI,CAAC;CACvB,MAAM,aAAa,IAAmB,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,SAAS,UAAU,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,qBAAqB,UAAU,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,YAAY,MAChB,aACM;EACJ,IAAI,uBAAuB;GACzB,IAAI,UAAU,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,kBAAkB;EAChB,MAAM;CACR,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;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
@@ -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.js","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,OAAO,YAAY,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,QAAQ,IAAI,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,OAPa,MACX,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,OAAO,eAClD,QAAQ,IAAG,CAAE,MAAM,KAAK,CAC1B;CAGF,OAAO;EACL;EACA,SAAS;CACX;AACF"}
1
+ {"version":3,"file":"useStore.js","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,OAAO,YAAY,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,QAAQ,IAAI,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,OAPa,MACX,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,OAAO,eACnD,QAAQ,IAAG,CAAE,MAAM,KAAK,CAC1B;CAGF,OAAO;EACL;EACA,SAAS;CACX;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
@@ -1 +1 @@
1
- {"version":3,"file":"useStoreDevtools.d.ts","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,CAAC,OAAO,GAAE;IACxC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CAClB,GAAG,sBAAsB,CAkH9B"}
1
+ {"version":3,"file":"useStoreDevtools.d.ts","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
  * ***********************************
@@ -1 +1 @@
1
- {"version":3,"file":"useStoreDevtools.js","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,OAAQ,eAAe,KAAK;AAC9B;AAEA,SAAS,cAAc,OAAqB,IAAuB;CACjE,OAAO;EACL;EACA,OAAO,UAAU,MAAM,MAAM;EAC7B,QAAQ;CACV;AACF;AAEO,SAAS,iBAAiB,UAK7B,CAAC,GAA2B;CAC9B,MAAM,EAAE,gBAAgB,QAAQ;CAEhC,MAAM,SAAS,IAAiB,CAAC,CAAC;CAClC,MAAM,WAAW,IAAqB,CAAC,CAAC;CACxC,MAAM,eAAe,IAAI,KAAK;CAE9B,IAAI,cAAmC;CAEvC,SAAS,gBAAsB;EAC7B,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO,IAAI;GACd,OAAO,QAAQ,CAAC;GAChB;EACF;EAEA,OAAO,QAAQ,MAAM,KAAK,MAAM,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,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,OAAO,UAAU,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,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO,IACV;EAGF,MAAM,GAAG,SAAS,UAAU;GAC1B,IAAI,OAAO,MAAM,WAAW,YAC1B,MAAM,OAAO;EAEjB,CAAC;EAED,cAAc;CAChB;CAEA,cAAc;CAEd,kBAAkB;EAChB,eAAe;CACjB,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useStoreDevtools.js","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,OAAQ,eAAe,KAAK;AAC9B;AAEA,SAAS,cAAc,OAAqB,IAAuB;CACjE,OAAO;EACL;EACA,OAAO,UAAU,MAAM,MAAM;EAC7B,QAAQ;CACV;AACF;AAEO,SAAS,iBACd,UAKI,CAAC,GACmB;CACxB,MAAM,EAAE,gBAAgB,QAAQ;CAEhC,MAAM,SAAS,IAAiB,CAAC,CAAC;CAClC,MAAM,WAAW,IAAqB,CAAC,CAAC;CACxC,MAAM,eAAe,IAAI,KAAK;CAE9B,IAAI,cAAmC;CAEvC,SAAS,gBAAsB;EAC7B,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO,IAAI;GACd,OAAO,QAAQ,CAAC;GAChB;EACF;EAEA,OAAO,QAAQ,MAAM,KAAK,MAAM,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,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,OAAO,UAAU,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,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO,IACV;EAGF,MAAM,GAAG,SAAQ,UAAS;GACxB,IAAI,OAAO,MAAM,WAAW,YAC1B,MAAM,OAAO;EAEjB,CAAC;EAED,cAAc;CAChB;CAEA,cAAc;CAEd,kBAAkB;EAChB,eAAe;CACjB,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"useStoreSnapshot.d.ts","sourceRoot":"","sources":["../../src/composables/useStoreSnapshot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,WAAW,EAAE,GAAG,EAAC,MAAM,KAAK,CAAC;AAI1C;;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,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAAA;IAClF,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;AAmHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,CAuO3B"}
1
+ {"version":3,"file":"useStoreSnapshot.d.ts","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"}