@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.
- package/LICENSE +21 -0
- package/README.md +993 -26
- package/dist/adapters/indexedDB.cjs +0 -1
- package/dist/adapters/indexedDB.mjs +0 -1
- package/dist/adapters/localStorage.cjs +0 -1
- package/dist/adapters/localStorage.mjs +0 -1
- package/dist/adapters/memory.cjs +0 -1
- package/dist/adapters/memory.mjs +0 -1
- package/dist/adapters/sessionStorage.cjs +0 -1
- package/dist/adapters/sessionStorage.mjs +0 -1
- package/dist/async-entry.d.ts +0 -1
- package/dist/async.cjs +0 -1
- package/dist/async.d.ts +0 -1
- package/dist/async.mjs +0 -1
- package/dist/batch.d.ts +0 -1
- package/dist/compose.d.ts +0 -1
- package/dist/computed-entry.d.ts +0 -1
- package/dist/computed.cjs +0 -1
- package/dist/computed.d.ts +0 -1
- package/dist/computed.mjs +0 -1
- package/dist/devtools/history.d.ts +0 -1
- package/dist/devtools/index.d.ts +0 -1
- package/dist/devtools/redux-bridge.d.ts +0 -1
- package/dist/devtools/snapshots.d.ts +0 -1
- package/dist/devtools/withDevtools.d.ts +0 -1
- package/dist/devtools.cjs +0 -1
- package/dist/devtools.mjs +0 -1
- package/dist/extensions/noop.d.ts +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/persist/adapters/indexedDB.d.ts +0 -1
- package/dist/persist/adapters/localStorage.d.ts +0 -1
- package/dist/persist/adapters/memory.d.ts +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts +0 -1
- package/dist/persist/debounce.d.ts +0 -1
- package/dist/persist/hydrate.d.ts +0 -1
- package/dist/persist/index.d.ts +0 -1
- package/dist/persist/serialize.d.ts +0 -1
- package/dist/persist.cjs +0 -1
- package/dist/persist.mjs +0 -1
- package/dist/proxy.d.ts +0 -1
- package/dist/registry-qtr1UpFU.js +0 -1
- package/dist/registry-zaKZ1P-s.js +0 -1
- package/dist/registry.d.ts +0 -1
- package/dist/signals/createSignal.d.ts +0 -1
- package/dist/signals/index.d.ts +0 -1
- package/dist/signals/useSignal.d.ts +0 -1
- package/dist/signals.cjs +0 -1
- package/dist/signals.mjs +0 -1
- package/dist/store.d.ts +0 -1
- package/dist/sync/channel.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/protocol.d.ts +0 -1
- package/dist/sync/withSync.d.ts +0 -1
- package/dist/sync.cjs +0 -1
- package/dist/sync.mjs +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +9 -3
- package/CHANGELOG.md +0 -151
- package/benchmarks/run.ts +0 -102
- package/benchmarks/week2.md +0 -9
- package/benchmarks/week2.ts +0 -64
- package/benchmarks/week4.md +0 -13
- package/benchmarks/week4.ts +0 -178
- package/benchmarks/week5.md +0 -15
- package/benchmarks/week5.ts +0 -184
- package/coverage/coverage-summary.json +0 -31
- package/dist/adapters/indexedDB.cjs.map +0 -1
- package/dist/adapters/indexedDB.mjs.map +0 -1
- package/dist/adapters/localStorage.cjs.map +0 -1
- package/dist/adapters/localStorage.mjs.map +0 -1
- package/dist/adapters/memory.cjs.map +0 -1
- package/dist/adapters/memory.mjs.map +0 -1
- package/dist/adapters/sessionStorage.cjs.map +0 -1
- package/dist/adapters/sessionStorage.mjs.map +0 -1
- package/dist/async-entry.d.ts.map +0 -1
- package/dist/async.cjs.map +0 -1
- package/dist/async.d.ts.map +0 -1
- package/dist/async.mjs.map +0 -1
- package/dist/batch.d.ts.map +0 -1
- package/dist/compose.d.ts.map +0 -1
- package/dist/computed-entry.d.ts.map +0 -1
- package/dist/computed.cjs.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/computed.mjs.map +0 -1
- package/dist/devtools/history.d.ts.map +0 -1
- package/dist/devtools/index.d.ts.map +0 -1
- package/dist/devtools/redux-bridge.d.ts.map +0 -1
- package/dist/devtools/snapshots.d.ts.map +0 -1
- package/dist/devtools/withDevtools.d.ts.map +0 -1
- package/dist/devtools.cjs.map +0 -1
- package/dist/devtools.mjs.map +0 -1
- package/dist/extensions/noop.d.ts.map +0 -1
- package/dist/index.cjs.js +0 -118
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -116
- package/dist/index.esm.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
- package/dist/persist/adapters/localStorage.d.ts.map +0 -1
- package/dist/persist/adapters/memory.d.ts.map +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
- package/dist/persist/debounce.d.ts.map +0 -1
- package/dist/persist/hydrate.d.ts.map +0 -1
- package/dist/persist/index.d.ts.map +0 -1
- package/dist/persist/serialize.d.ts.map +0 -1
- package/dist/persist.cjs.map +0 -1
- package/dist/persist.mjs.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/registry-D3X0HSbl.js +0 -26
- package/dist/registry-D3X0HSbl.js.map +0 -1
- package/dist/registry-RDjbeJdx.js +0 -29
- package/dist/registry-RDjbeJdx.js.map +0 -1
- package/dist/registry-qtr1UpFU.js.map +0 -1
- package/dist/registry-zaKZ1P-s.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/signals/createSignal.d.ts.map +0 -1
- package/dist/signals/index.d.ts.map +0 -1
- package/dist/signals/useSignal.d.ts.map +0 -1
- package/dist/signals.cjs.map +0 -1
- package/dist/signals.mjs.map +0 -1
- package/dist/stats.html +0 -4949
- package/dist/store.d.ts.map +0 -1
- package/dist/sync/channel.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/protocol.d.ts.map +0 -1
- package/dist/sync/withSync.d.ts.map +0 -1
- package/dist/sync.cjs.map +0 -1
- package/dist/sync.mjs.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/rollup.config.mjs +0 -44
- package/src/async-entry.ts +0 -6
- package/src/async.ts +0 -240
- package/src/batch.ts +0 -33
- package/src/compose.ts +0 -50
- package/src/computed-entry.ts +0 -6
- package/src/computed.ts +0 -187
- package/src/devtools/history.ts +0 -103
- package/src/devtools/index.ts +0 -5
- package/src/devtools/redux-bridge.ts +0 -70
- package/src/devtools/snapshots.ts +0 -54
- package/src/devtools/withDevtools.ts +0 -196
- package/src/extensions/noop.ts +0 -12
- package/src/index.ts +0 -4
- package/src/persist/adapters/indexedDB.ts +0 -114
- package/src/persist/adapters/localStorage.ts +0 -28
- package/src/persist/adapters/memory.ts +0 -26
- package/src/persist/adapters/sessionStorage.ts +0 -28
- package/src/persist/debounce.ts +0 -28
- package/src/persist/hydrate.ts +0 -60
- package/src/persist/index.ts +0 -141
- package/src/persist/serialize.ts +0 -60
- package/src/proxy.ts +0 -87
- package/src/registry.ts +0 -67
- package/src/signals/createSignal.ts +0 -81
- package/src/signals/index.ts +0 -20
- package/src/signals/useSignal.ts +0 -18
- package/src/store.ts +0 -250
- package/src/sync/channel.ts +0 -15
- package/src/sync/index.ts +0 -3
- package/src/sync/protocol.ts +0 -18
- package/src/sync/withSync.ts +0 -147
- package/src/types.ts +0 -159
- package/tests/async.test.ts +0 -1100
- package/tests/batch.test.ts +0 -41
- package/tests/compose.test.ts +0 -209
- package/tests/computed.test.ts +0 -867
- package/tests/devtools.test.ts +0 -1039
- package/tests/integration/persist.integration.test.ts +0 -258
- package/tests/integration/signals.integration.test.ts +0 -309
- package/tests/integration.test.ts +0 -278
- package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
- package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
- package/tests/persist/adapters/memory.adapter.test.ts +0 -112
- package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
- package/tests/persist/debounce.test.ts +0 -121
- package/tests/persist/hydrate.test.ts +0 -120
- package/tests/persist/migrate.test.ts +0 -208
- package/tests/persist/persist.test.ts +0 -357
- package/tests/persist/serialize.test.ts +0 -128
- package/tests/proxy.test.ts +0 -473
- package/tests/registry.test.ts +0 -67
- package/tests/signals/derived.test.ts +0 -244
- package/tests/signals/inference.test.ts +0 -108
- package/tests/signals/signal.test.ts +0 -348
- package/tests/signals/useSignal.test.tsx +0 -275
- package/tests/store.test.ts +0 -482
- package/tests/stress.test.ts +0 -268
- package/tests/sync.test.ts +0 -576
- package/tests/types.test.ts +0 -32
- package/tests/v0.3.test.ts +0 -813
- package/tree-shake-test.js +0 -1
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -22
- package/vitest_play.ts +0 -7
package/tests/sync.test.ts
DELETED
|
@@ -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
|
-
});
|
package/tests/types.test.ts
DELETED
|
@@ -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
|
-
});
|