@storve/core 1.0.0

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 (196) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/benchmarks/run.ts +102 -0
  3. package/benchmarks/week2.md +9 -0
  4. package/benchmarks/week2.ts +64 -0
  5. package/benchmarks/week4.md +13 -0
  6. package/benchmarks/week4.ts +178 -0
  7. package/benchmarks/week5.md +15 -0
  8. package/benchmarks/week5.ts +184 -0
  9. package/coverage/coverage-summary.json +31 -0
  10. package/dist/adapters/indexedDB.cjs +2 -0
  11. package/dist/adapters/indexedDB.cjs.map +1 -0
  12. package/dist/adapters/indexedDB.mjs +2 -0
  13. package/dist/adapters/indexedDB.mjs.map +1 -0
  14. package/dist/adapters/localStorage.cjs +2 -0
  15. package/dist/adapters/localStorage.cjs.map +1 -0
  16. package/dist/adapters/localStorage.mjs +2 -0
  17. package/dist/adapters/localStorage.mjs.map +1 -0
  18. package/dist/adapters/memory.cjs +2 -0
  19. package/dist/adapters/memory.cjs.map +1 -0
  20. package/dist/adapters/memory.mjs +2 -0
  21. package/dist/adapters/memory.mjs.map +1 -0
  22. package/dist/adapters/sessionStorage.cjs +2 -0
  23. package/dist/adapters/sessionStorage.cjs.map +1 -0
  24. package/dist/adapters/sessionStorage.mjs +2 -0
  25. package/dist/adapters/sessionStorage.mjs.map +1 -0
  26. package/dist/async-entry.d.ts +7 -0
  27. package/dist/async-entry.d.ts.map +1 -0
  28. package/dist/async.cjs +2 -0
  29. package/dist/async.cjs.map +1 -0
  30. package/dist/async.d.ts +52 -0
  31. package/dist/async.d.ts.map +1 -0
  32. package/dist/async.mjs +2 -0
  33. package/dist/async.mjs.map +1 -0
  34. package/dist/batch.d.ts +12 -0
  35. package/dist/batch.d.ts.map +1 -0
  36. package/dist/compose.d.ts +7 -0
  37. package/dist/compose.d.ts.map +1 -0
  38. package/dist/computed-entry.d.ts +7 -0
  39. package/dist/computed-entry.d.ts.map +1 -0
  40. package/dist/computed.cjs +2 -0
  41. package/dist/computed.cjs.map +1 -0
  42. package/dist/computed.d.ts +56 -0
  43. package/dist/computed.d.ts.map +1 -0
  44. package/dist/computed.mjs +2 -0
  45. package/dist/computed.mjs.map +1 -0
  46. package/dist/devtools/history.d.ts +51 -0
  47. package/dist/devtools/history.d.ts.map +1 -0
  48. package/dist/devtools/index.d.ts +5 -0
  49. package/dist/devtools/index.d.ts.map +1 -0
  50. package/dist/devtools/redux-bridge.d.ts +21 -0
  51. package/dist/devtools/redux-bridge.d.ts.map +1 -0
  52. package/dist/devtools/snapshots.d.ts +32 -0
  53. package/dist/devtools/snapshots.d.ts.map +1 -0
  54. package/dist/devtools/withDevtools.d.ts +17 -0
  55. package/dist/devtools/withDevtools.d.ts.map +1 -0
  56. package/dist/devtools.cjs +2 -0
  57. package/dist/devtools.cjs.map +1 -0
  58. package/dist/devtools.mjs +2 -0
  59. package/dist/devtools.mjs.map +1 -0
  60. package/dist/extensions/noop.d.ts +2 -0
  61. package/dist/extensions/noop.d.ts.map +1 -0
  62. package/dist/index.cjs +2 -0
  63. package/dist/index.cjs.js +118 -0
  64. package/dist/index.cjs.js.map +1 -0
  65. package/dist/index.cjs.map +1 -0
  66. package/dist/index.d.ts +5 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.esm.js +116 -0
  69. package/dist/index.esm.js.map +1 -0
  70. package/dist/index.mjs +2 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/persist/adapters/indexedDB.d.ts +12 -0
  73. package/dist/persist/adapters/indexedDB.d.ts.map +1 -0
  74. package/dist/persist/adapters/localStorage.d.ts +11 -0
  75. package/dist/persist/adapters/localStorage.d.ts.map +1 -0
  76. package/dist/persist/adapters/memory.d.ts +11 -0
  77. package/dist/persist/adapters/memory.d.ts.map +1 -0
  78. package/dist/persist/adapters/sessionStorage.d.ts +11 -0
  79. package/dist/persist/adapters/sessionStorage.d.ts.map +1 -0
  80. package/dist/persist/debounce.d.ts +12 -0
  81. package/dist/persist/debounce.d.ts.map +1 -0
  82. package/dist/persist/hydrate.d.ts +15 -0
  83. package/dist/persist/hydrate.d.ts.map +1 -0
  84. package/dist/persist/index.d.ts +34 -0
  85. package/dist/persist/index.d.ts.map +1 -0
  86. package/dist/persist/serialize.d.ts +28 -0
  87. package/dist/persist/serialize.d.ts.map +1 -0
  88. package/dist/persist.cjs +2 -0
  89. package/dist/persist.cjs.map +1 -0
  90. package/dist/persist.mjs +2 -0
  91. package/dist/persist.mjs.map +1 -0
  92. package/dist/proxy.d.ts +2 -0
  93. package/dist/proxy.d.ts.map +1 -0
  94. package/dist/registry-D3X0HSbl.js +26 -0
  95. package/dist/registry-D3X0HSbl.js.map +1 -0
  96. package/dist/registry-RDjbeJdx.js +29 -0
  97. package/dist/registry-RDjbeJdx.js.map +1 -0
  98. package/dist/registry-qtr1UpFU.js +2 -0
  99. package/dist/registry-qtr1UpFU.js.map +1 -0
  100. package/dist/registry-zaKZ1P-s.js +2 -0
  101. package/dist/registry-zaKZ1P-s.js.map +1 -0
  102. package/dist/registry.d.ts +54 -0
  103. package/dist/registry.d.ts.map +1 -0
  104. package/dist/signals/createSignal.d.ts +19 -0
  105. package/dist/signals/createSignal.d.ts.map +1 -0
  106. package/dist/signals/index.d.ts +20 -0
  107. package/dist/signals/index.d.ts.map +1 -0
  108. package/dist/signals/useSignal.d.ts +11 -0
  109. package/dist/signals/useSignal.d.ts.map +1 -0
  110. package/dist/signals.cjs +2 -0
  111. package/dist/signals.cjs.map +1 -0
  112. package/dist/signals.mjs +2 -0
  113. package/dist/signals.mjs.map +1 -0
  114. package/dist/stats.html +4949 -0
  115. package/dist/store.d.ts +12 -0
  116. package/dist/store.d.ts.map +1 -0
  117. package/dist/sync/channel.d.ts +7 -0
  118. package/dist/sync/channel.d.ts.map +1 -0
  119. package/dist/sync/index.d.ts +3 -0
  120. package/dist/sync/index.d.ts.map +1 -0
  121. package/dist/sync/protocol.d.ts +22 -0
  122. package/dist/sync/protocol.d.ts.map +1 -0
  123. package/dist/sync/withSync.d.ts +17 -0
  124. package/dist/sync/withSync.d.ts.map +1 -0
  125. package/dist/sync.cjs +2 -0
  126. package/dist/sync.cjs.map +1 -0
  127. package/dist/sync.mjs +2 -0
  128. package/dist/sync.mjs.map +1 -0
  129. package/dist/types.d.ts +134 -0
  130. package/dist/types.d.ts.map +1 -0
  131. package/package.json +91 -0
  132. package/rollup.config.mjs +44 -0
  133. package/src/async-entry.ts +6 -0
  134. package/src/async.ts +240 -0
  135. package/src/batch.ts +33 -0
  136. package/src/compose.ts +50 -0
  137. package/src/computed-entry.ts +6 -0
  138. package/src/computed.ts +187 -0
  139. package/src/devtools/history.ts +103 -0
  140. package/src/devtools/index.ts +5 -0
  141. package/src/devtools/redux-bridge.ts +70 -0
  142. package/src/devtools/snapshots.ts +54 -0
  143. package/src/devtools/withDevtools.ts +196 -0
  144. package/src/extensions/noop.ts +12 -0
  145. package/src/index.ts +4 -0
  146. package/src/persist/adapters/indexedDB.ts +114 -0
  147. package/src/persist/adapters/localStorage.ts +28 -0
  148. package/src/persist/adapters/memory.ts +26 -0
  149. package/src/persist/adapters/sessionStorage.ts +28 -0
  150. package/src/persist/debounce.ts +28 -0
  151. package/src/persist/hydrate.ts +60 -0
  152. package/src/persist/index.ts +141 -0
  153. package/src/persist/serialize.ts +60 -0
  154. package/src/proxy.ts +87 -0
  155. package/src/registry.ts +67 -0
  156. package/src/signals/createSignal.ts +81 -0
  157. package/src/signals/index.ts +20 -0
  158. package/src/signals/useSignal.ts +18 -0
  159. package/src/store.ts +250 -0
  160. package/src/sync/channel.ts +15 -0
  161. package/src/sync/index.ts +3 -0
  162. package/src/sync/protocol.ts +18 -0
  163. package/src/sync/withSync.ts +147 -0
  164. package/src/types.ts +159 -0
  165. package/tests/async.test.ts +1100 -0
  166. package/tests/batch.test.ts +41 -0
  167. package/tests/compose.test.ts +209 -0
  168. package/tests/computed.test.ts +867 -0
  169. package/tests/devtools.test.ts +1039 -0
  170. package/tests/integration/persist.integration.test.ts +258 -0
  171. package/tests/integration/signals.integration.test.ts +309 -0
  172. package/tests/integration.test.ts +278 -0
  173. package/tests/persist/adapters/indexedDB.adapter.test.ts +185 -0
  174. package/tests/persist/adapters/localStorage.adapter.test.ts +105 -0
  175. package/tests/persist/adapters/memory.adapter.test.ts +112 -0
  176. package/tests/persist/adapters/sessionStorage.adapter.test.ts +128 -0
  177. package/tests/persist/debounce.test.ts +121 -0
  178. package/tests/persist/hydrate.test.ts +120 -0
  179. package/tests/persist/migrate.test.ts +208 -0
  180. package/tests/persist/persist.test.ts +357 -0
  181. package/tests/persist/serialize.test.ts +128 -0
  182. package/tests/proxy.test.ts +473 -0
  183. package/tests/registry.test.ts +67 -0
  184. package/tests/signals/derived.test.ts +244 -0
  185. package/tests/signals/inference.test.ts +108 -0
  186. package/tests/signals/signal.test.ts +348 -0
  187. package/tests/signals/useSignal.test.tsx +275 -0
  188. package/tests/store.test.ts +482 -0
  189. package/tests/stress.test.ts +268 -0
  190. package/tests/sync.test.ts +576 -0
  191. package/tests/types.test.ts +32 -0
  192. package/tests/v0.3.test.ts +813 -0
  193. package/tree-shake-test.js +1 -0
  194. package/tsconfig.json +15 -0
  195. package/vitest.config.ts +22 -0
  196. package/vitest_play.ts +7 -0
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { batch, isBatching, subscribeToBatch } from '../src/batch'
3
+
4
+ describe('batch', () => {
5
+ it('isBatching returns false when not in batch', () => {
6
+ expect(isBatching()).toBe(false)
7
+ })
8
+
9
+ it('batch runs fn and notifies subscribers when batch ends', () => {
10
+ const cb = vi.fn()
11
+ const unsub = subscribeToBatch(cb)
12
+ expect(isBatching()).toBe(false)
13
+ batch(() => {
14
+ expect(isBatching()).toBe(true)
15
+ })
16
+ expect(isBatching()).toBe(false)
17
+ expect(cb).toHaveBeenCalledTimes(1)
18
+ unsub()
19
+ })
20
+
21
+ it('unsubscribe removes callback from batch end notifications', () => {
22
+ const cb = vi.fn()
23
+ const unsub = subscribeToBatch(cb)
24
+ batch(() => {})
25
+ expect(cb).toHaveBeenCalledTimes(1)
26
+ unsub()
27
+ batch(() => {})
28
+ expect(cb).toHaveBeenCalledTimes(1)
29
+ })
30
+
31
+ it('nested batch calls only notify when outermost batch ends', () => {
32
+ const cb = vi.fn()
33
+ const unsub = subscribeToBatch(cb)
34
+ batch(() => {
35
+ batch(() => {})
36
+ expect(cb).not.toHaveBeenCalled()
37
+ })
38
+ expect(cb).toHaveBeenCalledTimes(1)
39
+ unsub()
40
+ })
41
+ })
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { createStore } from '../src/store'
3
+ import { compose } from '../src/compose'
4
+ import { withPersist } from '../src/persist/index'
5
+ import { memoryAdapter } from '../src/persist/adapters/memory'
6
+
7
+ describe('compose', () => {
8
+ it('No enhancers: compose(store) returns the store unchanged', () => {
9
+ const store = createStore({ count: 0 })
10
+ const composed = compose(store)
11
+ expect(composed).toBe(store)
12
+ })
13
+
14
+ it('No enhancers: returned store is the exact same reference', () => {
15
+ const store = createStore({ a: 1 })
16
+ const composed = compose(store)
17
+ // toBe acts as exact reference check
18
+ expect(composed).toBe(store)
19
+ })
20
+
21
+ it('Single enhancer: compose(store, enhancerA) returns enhancerA(store)', () => {
22
+ const store = createStore({ count: 0 })
23
+ const enhancerA = vi.fn((s: typeof store) => ({ ...s, enhanced: true }))
24
+ const composed = compose(store, enhancerA)
25
+
26
+ expect(enhancerA).toHaveBeenCalledWith(store)
27
+ expect(composed).toHaveProperty('enhanced', true)
28
+ })
29
+
30
+ it('Single enhancer: returned store has the enhanced behaviour', () => {
31
+ const store = createStore({ count: 0 })
32
+ const enhancerA = <T extends object>(s: T) => ({ ...s, foo: 'bar' })
33
+ const composed = compose(store, enhancerA)
34
+ expect(composed).toHaveProperty('foo', 'bar')
35
+ })
36
+
37
+ it('Two enhancers: compose(store, enhancerA, enhancerB) applies A first, then B', () => {
38
+ const store = createStore({ num: 0 })
39
+ const calls: string[] = []
40
+
41
+ const enhancerA = <T extends object>(s: T) => {
42
+ calls.push('A')
43
+ // @ts-expect-error testing enhancement
44
+ const val = s.value ?? ''
45
+ return { ...s, value: val + 'A' }
46
+ }
47
+ const enhancerB = <T extends object>(s: T) => {
48
+ calls.push('B')
49
+ // @ts-expect-error testing enhancement
50
+ const val = s.value ?? ''
51
+ return { ...s, value: val + 'B' }
52
+ }
53
+
54
+ // spy enhancer
55
+ const composed = compose(store, enhancerA, enhancerB)
56
+ expect(calls).toEqual(['A', 'B'])
57
+ expect(composed).toHaveProperty('value', 'AB')
58
+ })
59
+
60
+ it('Two enhancers: enhancerB receives the output of enhancerA (not original store)', () => {
61
+ const store = createStore({ num: 0 })
62
+ const enhancerA = <T extends object>(s: T) => ({ ...s, stepA: true })
63
+ const enhancerB = vi.fn(<T extends object>(s: T) => ({ ...s, stepB: true }))
64
+
65
+ // spy enhancer
66
+ compose(store, enhancerA, enhancerB)
67
+ expect(enhancerB).toHaveBeenCalledWith(expect.objectContaining({ stepA: true }))
68
+ })
69
+
70
+ it('Two enhancers: order matters: compose(store, A, B) !== compose(store, B, A)', () => {
71
+ const store = createStore({ str: '' })
72
+ // @ts-expect-error dummy property
73
+ const A = <T extends object>(s: T) => ({ ...s, str: (s.str ?? '') + 'A' })
74
+ // @ts-expect-error dummy property
75
+ const B = <T extends object>(s: T) => ({ ...s, str: (s.str ?? '') + 'B' })
76
+
77
+ // spy enhancer
78
+ const ab = compose(store, A, B)
79
+ // spy enhancer
80
+ const ba = compose(store, B, A)
81
+
82
+ expect(ab).toHaveProperty('str', 'AB')
83
+ expect(ba).toHaveProperty('str', 'BA')
84
+ })
85
+
86
+ it('Three enhancers: all three are applied in order left to right', () => {
87
+ const store = createStore({ str: '' })
88
+ // @ts-expect-error dummy property
89
+ const A = <T extends object>(s: T) => ({ ...s, str: (s.str ?? '') + '1' })
90
+ // @ts-expect-error dummy property
91
+ const B = <T extends object>(s: T) => ({ ...s, str: (s.str ?? '') + '2' })
92
+ // @ts-expect-error dummy property
93
+ const C = <T extends object>(s: T) => ({ ...s, str: (s.str ?? '') + '3' })
94
+
95
+ // spy enhancer
96
+ const composed = compose(store, A, B, C)
97
+ expect(composed).toHaveProperty('str', '123')
98
+ })
99
+
100
+ it('Three enhancers: each enhancer receives the output of the previous one', () => {
101
+ const store = createStore({ count: 0 })
102
+ const A = vi.fn(<T extends object>(s: T) => ({ ...s, a: 1 }))
103
+ const B = vi.fn(<T extends object>(s: T) => ({ ...s, b: 2 }))
104
+ const C = vi.fn(<T extends object>(s: T) => ({ ...s, c: 3 }))
105
+
106
+ // spy enhancer
107
+ compose(store, A, B, C)
108
+
109
+ expect(B).toHaveBeenCalledWith(expect.objectContaining({ a: 1 }))
110
+ expect(C).toHaveBeenCalledWith(expect.objectContaining({ a: 1, b: 2 }))
111
+ })
112
+
113
+ describe('Enhancer correctness', () => {
114
+ it('enhancer that adds a method — method is present on composed store', () => {
115
+ const store = createStore({ count: 0 })
116
+ const addMethod = <T extends object>(s: T) => ({ ...s, doThing: () => 'done' })
117
+ // spy enhancer
118
+ const composed = compose(store, addMethod)
119
+ expect(composed.doThing()).toBe('done')
120
+ })
121
+
122
+ it('enhancer that wraps setState — wrapped behaviour fires correctly', () => {
123
+ const store = createStore({ count: 0 })
124
+ let intercepted = false
125
+ const wrapSetState = <T extends object>(s: T) => ({
126
+ ...s,
127
+ setState: (updater: unknown) => {
128
+ intercepted = true
129
+ // @ts-expect-error dynamic wrapper test
130
+ s.setState(updater)
131
+ }
132
+ })
133
+ // spy enhancer
134
+ const composed = compose(store, wrapSetState)
135
+ composed.setState({ count: 1 })
136
+
137
+ expect(intercepted).toBe(true)
138
+ expect(composed.getState()).toEqual({ count: 1 })
139
+ })
140
+
141
+ it('enhancer that wraps subscribe — subscriptions work correctly', () => {
142
+ const store = createStore({ count: 0 })
143
+ let customSubCalled = false
144
+ const wrapSubscribe = <T extends object>(s: T) => ({
145
+ ...s,
146
+ subscribe: (listener: unknown) => {
147
+ customSubCalled = true
148
+ // @ts-expect-error dynamic wrapper test
149
+ return s.subscribe(listener)
150
+ }
151
+ })
152
+
153
+ // spy enhancer
154
+ const composed = compose(store, wrapSubscribe)
155
+ composed.subscribe(() => {})
156
+
157
+ expect(customSubCalled).toBe(true)
158
+ })
159
+ })
160
+
161
+ describe('Real-world scenario', () => {
162
+ it('compose(createStore({count:0}), withPersist({key:\'x\', adapter: memoryAdapter(), debounce: 0})) produces a store with hydrated Promise', () => {
163
+ const adapter = memoryAdapter()
164
+ const composed = compose(
165
+ createStore({ count: 0 }),
166
+ withPersist({ key: 'x', adapter, debounce: 0 })
167
+ )
168
+
169
+ expect(composed.hydrated).toBeInstanceOf(Promise)
170
+ })
171
+
172
+ it('withPersist setState writes to adapter correctly through compose', async () => {
173
+ const adapter = memoryAdapter()
174
+ const composed = compose(
175
+ createStore({ count: 0 }),
176
+ withPersist({ key: 'x', adapter, debounce: 0 })
177
+ )
178
+
179
+ await composed.hydrated
180
+ composed.setState({ count: 42 })
181
+
182
+ const raw = await adapter.getItem('x')
183
+ expect(raw).toContain('"count":42')
184
+ })
185
+ })
186
+
187
+ describe('Type safety (runtime checks)', () => {
188
+ it('composed store still satisfies Store<T> interface', () => {
189
+ const store = createStore({ val: 'test' })
190
+ const composed = compose(store, s => s) // identity enhancer
191
+
192
+ expect(composed).toHaveProperty('getState')
193
+ expect(composed).toHaveProperty('setState')
194
+ expect(composed).toHaveProperty('subscribe')
195
+ })
196
+
197
+ it('getState, setState, subscribe all present and functional', () => {
198
+ const store = createStore({ count: 0 })
199
+ const composed = compose(store)
200
+
201
+ const sub = vi.fn()
202
+ composed.subscribe(sub)
203
+ composed.setState({ count: 5 })
204
+
205
+ expect(composed.getState()).toEqual({ count: 5 })
206
+ expect(sub).toHaveBeenCalledWith({ count: 5 })
207
+ })
208
+ })
209
+ })