@storve/core 1.0.1 → 1.0.3

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +993 -26
  3. package/dist/adapters/indexedDB.cjs +0 -1
  4. package/dist/adapters/indexedDB.mjs +0 -1
  5. package/dist/adapters/localStorage.cjs +0 -1
  6. package/dist/adapters/localStorage.mjs +0 -1
  7. package/dist/adapters/memory.cjs +0 -1
  8. package/dist/adapters/memory.mjs +0 -1
  9. package/dist/adapters/sessionStorage.cjs +0 -1
  10. package/dist/adapters/sessionStorage.mjs +0 -1
  11. package/dist/async-entry.d.ts +0 -1
  12. package/dist/async.cjs +0 -1
  13. package/dist/async.d.ts +0 -1
  14. package/dist/async.mjs +0 -1
  15. package/dist/batch.d.ts +0 -1
  16. package/dist/compose.d.ts +0 -1
  17. package/dist/computed-entry.d.ts +0 -1
  18. package/dist/computed.cjs +0 -1
  19. package/dist/computed.d.ts +0 -1
  20. package/dist/computed.mjs +0 -1
  21. package/dist/devtools/history.d.ts +0 -1
  22. package/dist/devtools/index.d.ts +0 -1
  23. package/dist/devtools/redux-bridge.d.ts +0 -1
  24. package/dist/devtools/snapshots.d.ts +0 -1
  25. package/dist/devtools/withDevtools.d.ts +0 -1
  26. package/dist/devtools.cjs +0 -1
  27. package/dist/devtools.mjs +0 -1
  28. package/dist/extensions/noop.d.ts +0 -1
  29. package/dist/index.cjs +0 -1
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.mjs +0 -1
  32. package/dist/persist/adapters/indexedDB.d.ts +0 -1
  33. package/dist/persist/adapters/localStorage.d.ts +0 -1
  34. package/dist/persist/adapters/memory.d.ts +0 -1
  35. package/dist/persist/adapters/sessionStorage.d.ts +0 -1
  36. package/dist/persist/debounce.d.ts +0 -1
  37. package/dist/persist/hydrate.d.ts +0 -1
  38. package/dist/persist/index.d.ts +0 -1
  39. package/dist/persist/serialize.d.ts +0 -1
  40. package/dist/persist.cjs +0 -1
  41. package/dist/persist.mjs +0 -1
  42. package/dist/proxy.d.ts +0 -1
  43. package/dist/registry-qtr1UpFU.js +0 -1
  44. package/dist/registry-zaKZ1P-s.js +0 -1
  45. package/dist/registry.d.ts +0 -1
  46. package/dist/signals/createSignal.d.ts +0 -1
  47. package/dist/signals/index.d.ts +0 -1
  48. package/dist/signals/useSignal.d.ts +0 -1
  49. package/dist/signals.cjs +0 -1
  50. package/dist/signals.mjs +0 -1
  51. package/dist/store.d.ts +0 -1
  52. package/dist/sync/channel.d.ts +0 -1
  53. package/dist/sync/index.d.ts +0 -1
  54. package/dist/sync/protocol.d.ts +0 -1
  55. package/dist/sync/withSync.d.ts +0 -1
  56. package/dist/sync.cjs +0 -1
  57. package/dist/sync.mjs +0 -1
  58. package/dist/types.d.ts +0 -1
  59. package/package.json +9 -3
  60. package/CHANGELOG.md +0 -151
  61. package/benchmarks/run.ts +0 -102
  62. package/benchmarks/week2.md +0 -9
  63. package/benchmarks/week2.ts +0 -64
  64. package/benchmarks/week4.md +0 -13
  65. package/benchmarks/week4.ts +0 -178
  66. package/benchmarks/week5.md +0 -15
  67. package/benchmarks/week5.ts +0 -184
  68. package/coverage/coverage-summary.json +0 -31
  69. package/dist/adapters/indexedDB.cjs.map +0 -1
  70. package/dist/adapters/indexedDB.mjs.map +0 -1
  71. package/dist/adapters/localStorage.cjs.map +0 -1
  72. package/dist/adapters/localStorage.mjs.map +0 -1
  73. package/dist/adapters/memory.cjs.map +0 -1
  74. package/dist/adapters/memory.mjs.map +0 -1
  75. package/dist/adapters/sessionStorage.cjs.map +0 -1
  76. package/dist/adapters/sessionStorage.mjs.map +0 -1
  77. package/dist/async-entry.d.ts.map +0 -1
  78. package/dist/async.cjs.map +0 -1
  79. package/dist/async.d.ts.map +0 -1
  80. package/dist/async.mjs.map +0 -1
  81. package/dist/batch.d.ts.map +0 -1
  82. package/dist/compose.d.ts.map +0 -1
  83. package/dist/computed-entry.d.ts.map +0 -1
  84. package/dist/computed.cjs.map +0 -1
  85. package/dist/computed.d.ts.map +0 -1
  86. package/dist/computed.mjs.map +0 -1
  87. package/dist/devtools/history.d.ts.map +0 -1
  88. package/dist/devtools/index.d.ts.map +0 -1
  89. package/dist/devtools/redux-bridge.d.ts.map +0 -1
  90. package/dist/devtools/snapshots.d.ts.map +0 -1
  91. package/dist/devtools/withDevtools.d.ts.map +0 -1
  92. package/dist/devtools.cjs.map +0 -1
  93. package/dist/devtools.mjs.map +0 -1
  94. package/dist/extensions/noop.d.ts.map +0 -1
  95. package/dist/index.cjs.js +0 -118
  96. package/dist/index.cjs.js.map +0 -1
  97. package/dist/index.cjs.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.esm.js +0 -116
  100. package/dist/index.esm.js.map +0 -1
  101. package/dist/index.mjs.map +0 -1
  102. package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
  103. package/dist/persist/adapters/localStorage.d.ts.map +0 -1
  104. package/dist/persist/adapters/memory.d.ts.map +0 -1
  105. package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
  106. package/dist/persist/debounce.d.ts.map +0 -1
  107. package/dist/persist/hydrate.d.ts.map +0 -1
  108. package/dist/persist/index.d.ts.map +0 -1
  109. package/dist/persist/serialize.d.ts.map +0 -1
  110. package/dist/persist.cjs.map +0 -1
  111. package/dist/persist.mjs.map +0 -1
  112. package/dist/proxy.d.ts.map +0 -1
  113. package/dist/registry-D3X0HSbl.js +0 -26
  114. package/dist/registry-D3X0HSbl.js.map +0 -1
  115. package/dist/registry-RDjbeJdx.js +0 -29
  116. package/dist/registry-RDjbeJdx.js.map +0 -1
  117. package/dist/registry-qtr1UpFU.js.map +0 -1
  118. package/dist/registry-zaKZ1P-s.js.map +0 -1
  119. package/dist/registry.d.ts.map +0 -1
  120. package/dist/signals/createSignal.d.ts.map +0 -1
  121. package/dist/signals/index.d.ts.map +0 -1
  122. package/dist/signals/useSignal.d.ts.map +0 -1
  123. package/dist/signals.cjs.map +0 -1
  124. package/dist/signals.mjs.map +0 -1
  125. package/dist/stats.html +0 -4949
  126. package/dist/store.d.ts.map +0 -1
  127. package/dist/sync/channel.d.ts.map +0 -1
  128. package/dist/sync/index.d.ts.map +0 -1
  129. package/dist/sync/protocol.d.ts.map +0 -1
  130. package/dist/sync/withSync.d.ts.map +0 -1
  131. package/dist/sync.cjs.map +0 -1
  132. package/dist/sync.mjs.map +0 -1
  133. package/dist/types.d.ts.map +0 -1
  134. package/rollup.config.mjs +0 -44
  135. package/src/async-entry.ts +0 -6
  136. package/src/async.ts +0 -240
  137. package/src/batch.ts +0 -33
  138. package/src/compose.ts +0 -50
  139. package/src/computed-entry.ts +0 -6
  140. package/src/computed.ts +0 -187
  141. package/src/devtools/history.ts +0 -103
  142. package/src/devtools/index.ts +0 -5
  143. package/src/devtools/redux-bridge.ts +0 -70
  144. package/src/devtools/snapshots.ts +0 -54
  145. package/src/devtools/withDevtools.ts +0 -196
  146. package/src/extensions/noop.ts +0 -12
  147. package/src/index.ts +0 -4
  148. package/src/persist/adapters/indexedDB.ts +0 -114
  149. package/src/persist/adapters/localStorage.ts +0 -28
  150. package/src/persist/adapters/memory.ts +0 -26
  151. package/src/persist/adapters/sessionStorage.ts +0 -28
  152. package/src/persist/debounce.ts +0 -28
  153. package/src/persist/hydrate.ts +0 -60
  154. package/src/persist/index.ts +0 -141
  155. package/src/persist/serialize.ts +0 -60
  156. package/src/proxy.ts +0 -87
  157. package/src/registry.ts +0 -67
  158. package/src/signals/createSignal.ts +0 -81
  159. package/src/signals/index.ts +0 -20
  160. package/src/signals/useSignal.ts +0 -18
  161. package/src/store.ts +0 -250
  162. package/src/sync/channel.ts +0 -15
  163. package/src/sync/index.ts +0 -3
  164. package/src/sync/protocol.ts +0 -18
  165. package/src/sync/withSync.ts +0 -147
  166. package/src/types.ts +0 -159
  167. package/tests/async.test.ts +0 -1100
  168. package/tests/batch.test.ts +0 -41
  169. package/tests/compose.test.ts +0 -209
  170. package/tests/computed.test.ts +0 -867
  171. package/tests/devtools.test.ts +0 -1039
  172. package/tests/integration/persist.integration.test.ts +0 -258
  173. package/tests/integration/signals.integration.test.ts +0 -309
  174. package/tests/integration.test.ts +0 -278
  175. package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
  176. package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
  177. package/tests/persist/adapters/memory.adapter.test.ts +0 -112
  178. package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
  179. package/tests/persist/debounce.test.ts +0 -121
  180. package/tests/persist/hydrate.test.ts +0 -120
  181. package/tests/persist/migrate.test.ts +0 -208
  182. package/tests/persist/persist.test.ts +0 -357
  183. package/tests/persist/serialize.test.ts +0 -128
  184. package/tests/proxy.test.ts +0 -473
  185. package/tests/registry.test.ts +0 -67
  186. package/tests/signals/derived.test.ts +0 -244
  187. package/tests/signals/inference.test.ts +0 -108
  188. package/tests/signals/signal.test.ts +0 -348
  189. package/tests/signals/useSignal.test.tsx +0 -275
  190. package/tests/store.test.ts +0 -482
  191. package/tests/stress.test.ts +0 -268
  192. package/tests/sync.test.ts +0 -576
  193. package/tests/types.test.ts +0 -32
  194. package/tests/v0.3.test.ts +0 -813
  195. package/tree-shake-test.js +0 -1
  196. package/tsconfig.json +0 -15
  197. package/vitest.config.ts +0 -22
  198. package/vitest_play.ts +0 -7
@@ -1,576 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { createStore } from '../src/store';
3
- import { withSync } from '../src/sync/withSync';
4
- import { tabId } from '../src/sync/protocol';
5
- import type { Store } from '../src/types';
6
-
7
- // ─── BroadcastChannel Mock ────────────────────────────────────────────────────
8
-
9
- class MockBroadcastChannel {
10
- name: string;
11
- onmessage: ((event: MessageEvent) => void) | null = null;
12
- postMessage = vi.fn();
13
- close = vi.fn();
14
-
15
- constructor(name: string) {
16
- this.name = name;
17
- MockBroadcastChannel._instances.set(name, this);
18
- }
19
-
20
- /** Simulate an incoming message from another tab */
21
- receive(data: object) {
22
- this.onmessage?.({ data } as MessageEvent);
23
- }
24
-
25
- static _instances = new Map<string, MockBroadcastChannel>();
26
-
27
- static reset() {
28
- MockBroadcastChannel._instances.clear();
29
- }
30
- }
31
-
32
- // ─── Helpers ──────────────────────────────────────────────────────────────────
33
-
34
- function makeStore(opts?: {
35
- keys?: string[];
36
- enabled?: boolean;
37
- channel?: string;
38
- }) {
39
- return createStore(
40
- withSync(
41
- { count: 0, label: 'init', local: 'tab-only' },
42
- {
43
- channel: opts?.channel ?? 'test-channel',
44
- keys: opts?.keys,
45
- enabled: opts?.enabled,
46
- }
47
- )
48
- );
49
- }
50
-
51
- function channelOf(store: ReturnType<typeof makeStore>): MockBroadcastChannel {
52
- return (store as Store<object> & { __sync_channel?: MockBroadcastChannel }).__sync_channel as MockBroadcastChannel;
53
- }
54
-
55
- /** Build a STATE_UPDATE message from another tab */
56
- function remoteUpdate(payload: object, fromTabId = 'other-tab-id') {
57
- return { type: 'STATE_UPDATE', payload, tabId: fromTabId };
58
- }
59
-
60
- /** Build a REQUEST_STATE message from another tab */
61
- function remoteRequest(fromTabId = 'other-tab-id') {
62
- return { type: 'REQUEST_STATE', tabId: fromTabId };
63
- }
64
-
65
- /** Build a PROVIDE_STATE message targeted at this tab */
66
- function remoteProvide(payload: object, fromTabId = 'other-tab-id') {
67
- return { type: 'PROVIDE_STATE', payload, targetTabId: tabId, tabId: fromTabId };
68
- }
69
-
70
- // ─────────────────────────────────────────────────────────────────────────────
71
- // Setup / Teardown
72
- // ─────────────────────────────────────────────────────────────────────────────
73
-
74
- beforeEach(() => {
75
- MockBroadcastChannel.reset();
76
- vi.stubGlobal('BroadcastChannel', MockBroadcastChannel);
77
- vi.stubGlobal('window', { BroadcastChannel: MockBroadcastChannel });
78
- });
79
-
80
- afterEach(() => {
81
- vi.unstubAllGlobals();
82
- vi.clearAllMocks();
83
- });
84
-
85
- // ─────────────────────────────────────────────────────────────────────────────
86
- // 1. SETUP
87
- // ─────────────────────────────────────────────────────────────────────────────
88
-
89
- describe('Setup', () => {
90
- it('opens a BroadcastChannel with the given name', () => {
91
- makeStore({ channel: 'my-channel' });
92
- expect(MockBroadcastChannel._instances.has('my-channel')).toBe(true);
93
- });
94
-
95
- it('exposes __sync_channel on the store', () => {
96
- const store = makeStore();
97
- expect(channelOf(store)).toBeInstanceOf(MockBroadcastChannel);
98
- });
99
-
100
- it('sends REQUEST_STATE on init', () => {
101
- const store = makeStore();
102
- const ch = channelOf(store);
103
- expect(ch.postMessage).toHaveBeenCalledWith(
104
- expect.objectContaining({ type: 'REQUEST_STATE', tabId })
105
- );
106
- });
107
-
108
- it('enabled: false — does not open a channel', () => {
109
- const store = makeStore({ enabled: false });
110
- expect((store as Store<object> & { __sync_channel?: MockBroadcastChannel }).__sync_channel).toBeUndefined();
111
- expect(MockBroadcastChannel._instances.size).toBe(0);
112
- });
113
-
114
- it('enabled: false — store works normally without sync', () => {
115
- const store = makeStore({ enabled: false });
116
- store.setState({ count: 5 });
117
- expect(store.getState().count).toBe(5);
118
- });
119
- });
120
-
121
- // ─────────────────────────────────────────────────────────────────────────────
122
- // 2. STATE_UPDATE — outgoing broadcasts
123
- // ─────────────────────────────────────────────────────────────────────────────
124
-
125
- describe('STATE_UPDATE — outgoing', () => {
126
- it('broadcasts STATE_UPDATE after setState', () => {
127
- const store = makeStore();
128
- const ch = channelOf(store);
129
- ch.postMessage.mockClear();
130
- store.setState({ count: 1 });
131
- expect(ch.postMessage).toHaveBeenCalledWith(
132
- expect.objectContaining({ type: 'STATE_UPDATE' })
133
- );
134
- });
135
-
136
- it('broadcast payload contains the changed key', () => {
137
- const store = makeStore();
138
- const ch = channelOf(store);
139
- ch.postMessage.mockClear();
140
- store.setState({ count: 5 });
141
- const call = ch.postMessage.mock.calls.find(
142
- ([msg]) => msg.type === 'STATE_UPDATE'
143
- );
144
- expect(call?.[0].payload.count).toBe(5);
145
- });
146
-
147
- it('broadcast includes tabId', () => {
148
- const store = makeStore();
149
- const ch = channelOf(store);
150
- ch.postMessage.mockClear();
151
- store.setState({ count: 1 });
152
- const call = ch.postMessage.mock.calls.find(
153
- ([msg]) => msg.type === 'STATE_UPDATE'
154
- );
155
- expect(call?.[0].tabId).toBe(tabId);
156
- });
157
-
158
- it('does NOT broadcast if state did not change', () => {
159
- const store = makeStore();
160
- const ch = channelOf(store);
161
- ch.postMessage.mockClear();
162
- store.setState({ count: 0 }); // same as initial
163
- const updates = ch.postMessage.mock.calls.filter(
164
- ([msg]) => msg.type === 'STATE_UPDATE'
165
- );
166
- expect(updates).toHaveLength(0);
167
- });
168
-
169
- it('only broadcasts changed keys — not the full state', () => {
170
- const store = makeStore();
171
- const ch = channelOf(store);
172
- ch.postMessage.mockClear();
173
- store.setState({ count: 1 });
174
- const call = ch.postMessage.mock.calls.find(
175
- ([msg]) => msg.type === 'STATE_UPDATE'
176
- );
177
- expect(call?.[0].payload).not.toHaveProperty('label');
178
- expect(call?.[0].payload).not.toHaveProperty('local');
179
- });
180
-
181
- it('broadcasts all changed keys in a single setState', () => {
182
- const store = makeStore();
183
- const ch = channelOf(store);
184
- ch.postMessage.mockClear();
185
- store.setState({ count: 1, label: 'updated' });
186
- const call = ch.postMessage.mock.calls.find(
187
- ([msg]) => msg.type === 'STATE_UPDATE'
188
- );
189
- expect(call?.[0].payload).toHaveProperty('count', 1);
190
- expect(call?.[0].payload).toHaveProperty('label', 'updated');
191
- });
192
-
193
- it('broadcasts once per setState call', () => {
194
- const store = makeStore();
195
- const ch = channelOf(store);
196
- ch.postMessage.mockClear();
197
- store.setState({ count: 1 });
198
- const updates = ch.postMessage.mock.calls.filter(
199
- ([msg]) => msg.type === 'STATE_UPDATE'
200
- );
201
- expect(updates).toHaveLength(1);
202
- });
203
- });
204
-
205
- // ─────────────────────────────────────────────────────────────────────────────
206
- // 3. SELECTIVE KEY SYNC
207
- // ─────────────────────────────────────────────────────────────────────────────
208
-
209
- describe('Selective key sync', () => {
210
- it('only broadcasts specified keys', () => {
211
- const store = makeStore({ keys: ['count'] });
212
- const ch = channelOf(store);
213
- ch.postMessage.mockClear();
214
- store.setState({ count: 1, label: 'changed' });
215
- const call = ch.postMessage.mock.calls.find(
216
- ([msg]) => msg.type === 'STATE_UPDATE'
217
- );
218
- expect(call?.[0].payload).toHaveProperty('count', 1);
219
- expect(call?.[0].payload).not.toHaveProperty('label');
220
- });
221
-
222
- it('does not broadcast if only non-synced keys changed', () => {
223
- const store = makeStore({ keys: ['count'] });
224
- const ch = channelOf(store);
225
- ch.postMessage.mockClear();
226
- store.setState({ label: 'changed' }); // label not in keys
227
- const updates = ch.postMessage.mock.calls.filter(
228
- ([msg]) => msg.type === 'STATE_UPDATE'
229
- );
230
- expect(updates).toHaveLength(0);
231
- });
232
-
233
- it('receiving STATE_UPDATE applies only the provided keys', () => {
234
- const store = makeStore({ keys: ['count'] });
235
- const ch = channelOf(store);
236
- ch.receive(remoteUpdate({ count: 99 }));
237
- expect(store.getState().count).toBe(99);
238
- expect(store.getState().label).toBe('init'); // untouched
239
- });
240
-
241
- it('PROVIDE_STATE response only includes synced keys', () => {
242
- const store = makeStore({ keys: ['count', 'label'] });
243
- const ch = channelOf(store);
244
- ch.postMessage.mockClear();
245
- ch.receive(remoteRequest());
246
- const call = ch.postMessage.mock.calls.find(
247
- ([msg]) => msg.type === 'PROVIDE_STATE'
248
- );
249
- expect(call?.[0].payload).toHaveProperty('count');
250
- expect(call?.[0].payload).toHaveProperty('label');
251
- expect(call?.[0].payload).not.toHaveProperty('local');
252
- });
253
- });
254
-
255
- // ─────────────────────────────────────────────────────────────────────────────
256
- // 4. INFINITE LOOP PREVENTION
257
- // ─────────────────────────────────────────────────────────────────────────────
258
-
259
- describe('Infinite loop prevention', () => {
260
- it('does not re-broadcast a received STATE_UPDATE', () => {
261
- const store = makeStore();
262
- const ch = channelOf(store);
263
- ch.postMessage.mockClear();
264
- ch.receive(remoteUpdate({ count: 42 }));
265
- const updates = ch.postMessage.mock.calls.filter(
266
- ([msg]) => msg.type === 'STATE_UPDATE'
267
- );
268
- expect(updates).toHaveLength(0);
269
- });
270
-
271
- it('applies received STATE_UPDATE to local store', () => {
272
- const store = makeStore();
273
- const ch = channelOf(store);
274
- ch.receive(remoteUpdate({ count: 42 }));
275
- expect(store.getState().count).toBe(42);
276
- });
277
-
278
- it('ignores STATE_UPDATE from own tabId', () => {
279
- const store = makeStore();
280
- const ch = channelOf(store);
281
- const before = store.getState().count;
282
- ch.receive({ type: 'STATE_UPDATE', payload: { count: 99 }, tabId });
283
- expect(store.getState().count).toBe(before);
284
- });
285
-
286
- it('local setState after receiving remote update broadcasts normally', () => {
287
- const store = makeStore();
288
- const ch = channelOf(store);
289
- ch.receive(remoteUpdate({ count: 5 }));
290
- ch.postMessage.mockClear();
291
- store.setState({ count: 6 });
292
- const updates = ch.postMessage.mock.calls.filter(
293
- ([msg]) => msg.type === 'STATE_UPDATE'
294
- );
295
- expect(updates).toHaveLength(1);
296
- });
297
-
298
- it('multiple rapid remote updates all apply correctly', () => {
299
- const store = makeStore();
300
- const ch = channelOf(store);
301
- ch.receive(remoteUpdate({ count: 1 }));
302
- ch.receive(remoteUpdate({ count: 2 }));
303
- ch.receive(remoteUpdate({ count: 3 }));
304
- expect(store.getState().count).toBe(3);
305
- const updates = ch.postMessage.mock.calls.filter(
306
- ([msg]) => msg.type === 'STATE_UPDATE'
307
- );
308
- expect(updates).toHaveLength(0);
309
- });
310
- });
311
-
312
- // ─────────────────────────────────────────────────────────────────────────────
313
- // 5. REHYDRATION — REQUEST_STATE / PROVIDE_STATE
314
- // ─────────────────────────────────────────────────────────────────────────────
315
-
316
- describe('Rehydration', () => {
317
- it('sends REQUEST_STATE on init', () => {
318
- const store = makeStore();
319
- const ch = channelOf(store);
320
- expect(ch.postMessage).toHaveBeenCalledWith(
321
- expect.objectContaining({ type: 'REQUEST_STATE', tabId })
322
- );
323
- });
324
-
325
- it('applies PROVIDE_STATE targeted at this tab', () => {
326
- const store = makeStore();
327
- const ch = channelOf(store);
328
- ch.receive(remoteProvide({ count: 7, label: 'hydrated' }));
329
- expect(store.getState().count).toBe(7);
330
- expect(store.getState().label).toBe('hydrated');
331
- });
332
-
333
- it('ignores PROVIDE_STATE targeted at a different tab', () => {
334
- const store = makeStore();
335
- const ch = channelOf(store);
336
- ch.receive({
337
- type: 'PROVIDE_STATE',
338
- payload: { count: 99 },
339
- targetTabId: 'some-other-tab',
340
- tabId: 'other-tab-id',
341
- });
342
- expect(store.getState().count).toBe(0);
343
- });
344
-
345
- it('only applies the first PROVIDE_STATE — subsequent ones ignored', () => {
346
- const store = makeStore();
347
- const ch = channelOf(store);
348
- ch.receive(remoteProvide({ count: 7 }));
349
- ch.receive(remoteProvide({ count: 99 }, 'yet-another-tab'));
350
- expect(store.getState().count).toBe(7);
351
- });
352
-
353
- it('PROVIDE_STATE does not re-broadcast', () => {
354
- const store = makeStore();
355
- const ch = channelOf(store);
356
- ch.postMessage.mockClear();
357
- ch.receive(remoteProvide({ count: 7 }));
358
- const updates = ch.postMessage.mock.calls.filter(
359
- ([msg]) => msg.type === 'STATE_UPDATE'
360
- );
361
- expect(updates).toHaveLength(0);
362
- });
363
-
364
- it('ignores PROVIDE_STATE from own tabId', () => {
365
- const store = makeStore();
366
- const ch = channelOf(store);
367
- ch.receive({
368
- type: 'PROVIDE_STATE',
369
- payload: { count: 99 },
370
- targetTabId: tabId,
371
- tabId, // from self
372
- });
373
- expect(store.getState().count).toBe(0);
374
- });
375
-
376
- it('responds to REQUEST_STATE from another tab', () => {
377
- const store = makeStore();
378
- store.setState({ count: 5 });
379
- const ch = channelOf(store);
380
- ch.postMessage.mockClear();
381
- ch.receive(remoteRequest());
382
- expect(ch.postMessage).toHaveBeenCalledWith(
383
- expect.objectContaining({
384
- type: 'PROVIDE_STATE',
385
- targetTabId: 'other-tab-id',
386
- tabId,
387
- })
388
- );
389
- });
390
-
391
- it('PROVIDE_STATE response contains current state', () => {
392
- const store = makeStore();
393
- store.setState({ count: 5, label: 'live' });
394
- const ch = channelOf(store);
395
- ch.postMessage.mockClear();
396
- ch.receive(remoteRequest());
397
- const call = ch.postMessage.mock.calls.find(
398
- ([msg]) => msg.type === 'PROVIDE_STATE'
399
- );
400
- expect(call?.[0].payload.count).toBe(5);
401
- expect(call?.[0].payload.label).toBe('live');
402
- });
403
-
404
- it('ignores REQUEST_STATE from own tabId', () => {
405
- const store = makeStore();
406
- const ch = channelOf(store);
407
- ch.postMessage.mockClear();
408
- ch.receive({ type: 'REQUEST_STATE', tabId }); // from self
409
- const responses = ch.postMessage.mock.calls.filter(
410
- ([msg]) => msg.type === 'PROVIDE_STATE'
411
- );
412
- expect(responses).toHaveLength(0);
413
- });
414
- });
415
-
416
- // ─────────────────────────────────────────────────────────────────────────────
417
- // 6. SSR & BROWSER COMPATIBILITY
418
- // ─────────────────────────────────────────────────────────────────────────────
419
-
420
- describe('SSR & browser compatibility', () => {
421
- it('no crash when window is undefined', () => {
422
- vi.stubGlobal('window', undefined);
423
- expect(() => makeStore()).not.toThrow();
424
- });
425
-
426
- it('no crash when BroadcastChannel is undefined', () => {
427
- vi.stubGlobal('window', {});
428
- vi.stubGlobal('BroadcastChannel', undefined);
429
- expect(() => makeStore()).not.toThrow();
430
- });
431
-
432
- it('store works normally when channel unavailable', () => {
433
- vi.stubGlobal('window', undefined);
434
- const store = makeStore();
435
- store.setState({ count: 5 });
436
- expect(store.getState().count).toBe(5);
437
- });
438
-
439
- it('__sync_channel is undefined when channel unavailable', () => {
440
- vi.stubGlobal('window', undefined);
441
- const store = makeStore();
442
- expect((store as Store<object> & { __sync_channel?: MockBroadcastChannel }).__sync_channel).toBeUndefined();
443
- });
444
- });
445
-
446
- // ─────────────────────────────────────────────────────────────────────────────
447
- // 7. MULTIPLE STORES & CHANNEL ISOLATION
448
- // ─────────────────────────────────────────────────────────────────────────────
449
-
450
- describe('Multiple stores & channel isolation', () => {
451
- it('two stores on different channels do not interfere', () => {
452
- const storeA = makeStore({ channel: 'channel-a' });
453
- const storeB = makeStore({ channel: 'channel-b' });
454
- const chA = channelOf(storeA);
455
- const chB = channelOf(storeB);
456
-
457
- // Simulate a remote update on channel-a's mock
458
- chA.receive(remoteUpdate({ count: 10 }));
459
-
460
- expect(storeA.getState().count).toBe(10);
461
- expect(storeB.getState().count).toBe(0); // unaffected
462
- // channel-b should not have sent anything
463
- const bUpdates = chB.postMessage.mock.calls.filter(
464
- ([msg]) => msg.type === 'STATE_UPDATE'
465
- );
466
- expect(bUpdates).toHaveLength(0);
467
- });
468
-
469
- it('two stores on same channel name share an instance', () => {
470
- makeStore({ channel: 'shared' });
471
- makeStore({ channel: 'shared' });
472
- // Both reference the same channel name — last one wins in _instances
473
- expect(MockBroadcastChannel._instances.has('shared')).toBe(true);
474
- });
475
-
476
- it('local setState on storeA does not affect storeB state', () => {
477
- const storeA = makeStore({ channel: 'a' });
478
- const storeB = makeStore({ channel: 'b' });
479
- storeA.setState({ count: 99 });
480
- expect(storeB.getState().count).toBe(0);
481
- });
482
- });
483
-
484
- // ─────────────────────────────────────────────────────────────────────────────
485
- // 8. SUBSCRIBERS & REACTIVITY
486
- // ─────────────────────────────────────────────────────────────────────────────
487
-
488
- describe('Subscribers & reactivity', () => {
489
- it('notifies subscribers on remote STATE_UPDATE', () => {
490
- const store = makeStore();
491
- const ch = channelOf(store);
492
- const listener = vi.fn();
493
- store.subscribe(listener);
494
- ch.receive(remoteUpdate({ count: 5 }));
495
- expect(listener).toHaveBeenCalledOnce();
496
- });
497
-
498
- it('notifies subscribers on PROVIDE_STATE rehydration', () => {
499
- const store = makeStore();
500
- const ch = channelOf(store);
501
- const listener = vi.fn();
502
- store.subscribe(listener);
503
- ch.receive(remoteProvide({ count: 3 }));
504
- expect(listener).toHaveBeenCalledOnce();
505
- });
506
-
507
- it('does not double-notify on local setState', () => {
508
- const store = makeStore();
509
- const listener = vi.fn();
510
- store.subscribe(listener);
511
- store.setState({ count: 1 });
512
- expect(listener).toHaveBeenCalledTimes(1);
513
- });
514
- });
515
-
516
- // ─────────────────────────────────────────────────────────────────────────────
517
- // 9. EDGE CASES
518
- // ─────────────────────────────────────────────────────────────────────────────
519
-
520
- describe('Edge cases', () => {
521
- it('handles empty payload in STATE_UPDATE gracefully', () => {
522
- const store = makeStore();
523
- const ch = channelOf(store);
524
- expect(() => ch.receive(remoteUpdate({}))).not.toThrow();
525
- expect(store.getState().count).toBe(0);
526
- });
527
-
528
- it('handles unknown message type gracefully', () => {
529
- const store = makeStore();
530
- const ch = channelOf(store);
531
- expect(() =>
532
- ch.receive({ type: 'UNKNOWN_TYPE', tabId: 'other' })
533
- ).not.toThrow();
534
- });
535
-
536
- it('rapid local setStates each broadcast independently', () => {
537
- const store = makeStore();
538
- const ch = channelOf(store);
539
- ch.postMessage.mockClear();
540
- store.setState({ count: 1 });
541
- store.setState({ count: 2 });
542
- store.setState({ count: 3 });
543
- const updates = ch.postMessage.mock.calls.filter(
544
- ([msg]) => msg.type === 'STATE_UPDATE'
545
- );
546
- expect(updates).toHaveLength(3);
547
- });
548
-
549
- it('setState with function updater still broadcasts', () => {
550
- const store = makeStore();
551
- const ch = channelOf(store);
552
- ch.postMessage.mockClear();
553
- store.setState((s: { count: number }) => ({ count: s.count + 1 }));
554
- const updates = ch.postMessage.mock.calls.filter(
555
- ([msg]) => msg.type === 'STATE_UPDATE'
556
- );
557
- expect(updates).toHaveLength(1);
558
- expect(updates[0][0].payload.count).toBe(1);
559
- });
560
-
561
- it('interleaved local and remote updates maintain correct final state', () => {
562
- const store = makeStore();
563
- const ch = channelOf(store);
564
- store.setState({ count: 1 });
565
- ch.receive(remoteUpdate({ count: 10 }));
566
- store.setState({ count: 2 });
567
- ch.receive(remoteUpdate({ count: 20 }));
568
- expect(store.getState().count).toBe(20);
569
- });
570
-
571
- it('withSync does not alter other store keys not involved in sync', () => {
572
- const store = makeStore({ keys: ['count'] });
573
- store.setState({ label: 'changed-locally' });
574
- expect(store.getState().label).toBe('changed-locally');
575
- });
576
- });
@@ -1,32 +0,0 @@
1
- import { describe, it, expectTypeOf } from 'vitest';
2
- import { createStore } from '../src/store';
3
-
4
- describe('TypeScript Type Inference', () => {
5
- it('infers store type correctly', () => {
6
- const store = createStore({ count: 0, text: 'hello' });
7
-
8
- expectTypeOf(store.getState).returns.toEqualTypeOf<{ count: number; text: string }>();
9
-
10
- expectTypeOf(store.setState).parameter(0).toMatchTypeOf<Partial<{ count: number; text: string }> | ((state: { count: number; text: string }) => Partial<{ count: number; text: string }>)>();
11
- });
12
-
13
- it('infers deeply nested types correctly', () => {
14
- const store = createStore({ nested: { a: 1, b: 'b' } });
15
-
16
- expectTypeOf(store.getState().nested.a).toBeNumber();
17
- expectTypeOf(store.getState().nested.b).toBeString();
18
- });
19
-
20
- it('prevents invalid updates (compile time)', () => {
21
- const store = createStore({ count: 0, text: 'hello' });
22
-
23
- // @ts-expect-error — count must be a number, assigning string should fail
24
- store.setState({ count: 'string' });
25
-
26
- // @ts-expect-error — 'unknown' is not a key of the store definition
27
- store.setState({ unknown: true });
28
-
29
- // @ts-expect-error — text must be string, assigning number should fail
30
- store.setState(() => ({ text: 42 }));
31
- });
32
- });