@voidhash/mimic-react 0.0.1-alpha.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.
- package/.turbo/turbo-build.log +19 -0
- package/LICENSE.md +663 -0
- package/dist/index.cjs +0 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +34 -0
- package/src/index.ts +0 -0
- package/src/zustand/index.ts +24 -0
- package/src/zustand/middleware.ts +171 -0
- package/src/zustand/types.ts +117 -0
- package/tests/zustand/middleware.test.ts +584 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createStore } from "zustand";
|
|
3
|
+
import { mimic } from "../../src/zustand/middleware";
|
|
4
|
+
import type { ClientDocument } from "@voidhash/mimic/client";
|
|
5
|
+
import type { Primitive, Presence } from "@voidhash/mimic";
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Mock ClientDocument
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
interface MockClientDocumentState {
|
|
12
|
+
snapshot: { title: string; count: number };
|
|
13
|
+
isConnected: boolean;
|
|
14
|
+
isReady: boolean;
|
|
15
|
+
pendingCount: number;
|
|
16
|
+
hasPendingChanges: boolean;
|
|
17
|
+
// Presence
|
|
18
|
+
presenceEnabled: boolean;
|
|
19
|
+
presenceSelfId?: string;
|
|
20
|
+
presenceSelf?: unknown;
|
|
21
|
+
presenceOthers: Map<string, Presence.PresenceEntry<unknown>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MockClientDocument {
|
|
25
|
+
root: { toSnapshot: () => { title: string; count: number } };
|
|
26
|
+
isConnected: () => boolean;
|
|
27
|
+
isReady: () => boolean;
|
|
28
|
+
getPendingCount: () => number;
|
|
29
|
+
hasPendingChanges: () => boolean;
|
|
30
|
+
connect: () => void;
|
|
31
|
+
subscribe: (listener: {
|
|
32
|
+
onStateChange?: () => void;
|
|
33
|
+
onConnectionChange?: () => void;
|
|
34
|
+
onReady?: () => void;
|
|
35
|
+
}) => () => void;
|
|
36
|
+
presence?: {
|
|
37
|
+
selfId: () => string | undefined;
|
|
38
|
+
self: () => unknown | undefined;
|
|
39
|
+
others: () => ReadonlyMap<string, Presence.PresenceEntry<unknown>>;
|
|
40
|
+
all: () => ReadonlyMap<string, Presence.PresenceEntry<unknown>>;
|
|
41
|
+
subscribe: (listener: { onPresenceChange?: () => void }) => () => void;
|
|
42
|
+
};
|
|
43
|
+
// Helpers to update state and trigger events
|
|
44
|
+
_setState: (updates: Partial<MockClientDocumentState>) => void;
|
|
45
|
+
_triggerStateChange: () => void;
|
|
46
|
+
_triggerConnectionChange: () => void;
|
|
47
|
+
_triggerReady: () => void;
|
|
48
|
+
_triggerPresenceChange: () => void;
|
|
49
|
+
_getSubscriberCount: () => number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const createMockClientDocument = (
|
|
53
|
+
initial?: Partial<MockClientDocumentState>
|
|
54
|
+
): MockClientDocument => {
|
|
55
|
+
let state: MockClientDocumentState = {
|
|
56
|
+
snapshot: { title: "Test", count: 0 },
|
|
57
|
+
isConnected: false,
|
|
58
|
+
isReady: false,
|
|
59
|
+
pendingCount: 0,
|
|
60
|
+
hasPendingChanges: false,
|
|
61
|
+
presenceEnabled: false,
|
|
62
|
+
presenceOthers: new Map(),
|
|
63
|
+
...initial,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const listeners = new Set<{
|
|
67
|
+
onStateChange?: () => void;
|
|
68
|
+
onConnectionChange?: () => void;
|
|
69
|
+
onReady?: () => void;
|
|
70
|
+
}>();
|
|
71
|
+
|
|
72
|
+
const presenceListeners = new Set<{ onPresenceChange?: () => void }>();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
root: {
|
|
76
|
+
toSnapshot: () => state.snapshot,
|
|
77
|
+
},
|
|
78
|
+
isConnected: () => state.isConnected,
|
|
79
|
+
isReady: () => state.isReady,
|
|
80
|
+
getPendingCount: () => state.pendingCount,
|
|
81
|
+
hasPendingChanges: () => state.hasPendingChanges,
|
|
82
|
+
connect: () => {
|
|
83
|
+
// Mock connect - does nothing in tests
|
|
84
|
+
},
|
|
85
|
+
subscribe: (listener) => {
|
|
86
|
+
listeners.add(listener);
|
|
87
|
+
return () => {
|
|
88
|
+
listeners.delete(listener);
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
presence: state.presenceEnabled
|
|
92
|
+
? {
|
|
93
|
+
selfId: () => state.presenceSelfId,
|
|
94
|
+
self: () => state.presenceSelf,
|
|
95
|
+
others: () => state.presenceOthers,
|
|
96
|
+
all: () => {
|
|
97
|
+
const all = new Map<string, Presence.PresenceEntry<unknown>>();
|
|
98
|
+
for (const [id, entry] of state.presenceOthers) {
|
|
99
|
+
all.set(id, entry);
|
|
100
|
+
}
|
|
101
|
+
if (state.presenceSelfId !== undefined && state.presenceSelf !== undefined) {
|
|
102
|
+
all.set(state.presenceSelfId, { data: state.presenceSelf });
|
|
103
|
+
}
|
|
104
|
+
return all;
|
|
105
|
+
},
|
|
106
|
+
subscribe: (listener) => {
|
|
107
|
+
presenceListeners.add(listener);
|
|
108
|
+
return () => {
|
|
109
|
+
presenceListeners.delete(listener);
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
: undefined,
|
|
114
|
+
_setState: (updates) => {
|
|
115
|
+
state = { ...state, ...updates };
|
|
116
|
+
},
|
|
117
|
+
_triggerStateChange: () => {
|
|
118
|
+
for (const listener of listeners) {
|
|
119
|
+
listener.onStateChange?.();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
_triggerConnectionChange: () => {
|
|
123
|
+
for (const listener of listeners) {
|
|
124
|
+
listener.onConnectionChange?.();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
_triggerReady: () => {
|
|
128
|
+
for (const listener of listeners) {
|
|
129
|
+
listener.onReady?.();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
_triggerPresenceChange: () => {
|
|
133
|
+
for (const listener of presenceListeners) {
|
|
134
|
+
listener.onPresenceChange?.();
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
_getSubscriberCount: () => listeners.size,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// Test Schema Type (for type inference)
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
type TestSchema = Primitive.Primitive<
|
|
146
|
+
{ title: string; count: number },
|
|
147
|
+
{ toSnapshot: () => { title: string; count: number } }
|
|
148
|
+
>;
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Tests
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
describe("mimic middleware", () => {
|
|
155
|
+
let mockDocument: MockClientDocument;
|
|
156
|
+
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
mockDocument = createMockClientDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("Basic Integration", () => {
|
|
162
|
+
it("should create store with initial mimic slice", () => {
|
|
163
|
+
mockDocument._setState({
|
|
164
|
+
snapshot: { title: "Initial", count: 42 },
|
|
165
|
+
isConnected: true,
|
|
166
|
+
isReady: true,
|
|
167
|
+
pendingCount: 0,
|
|
168
|
+
hasPendingChanges: false,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const store = createStore(
|
|
172
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const state = store.getState();
|
|
176
|
+
|
|
177
|
+
expect(state.mimic).toBeDefined();
|
|
178
|
+
expect(state.mimic.snapshot).toEqual({ title: "Initial", count: 42 });
|
|
179
|
+
expect(state.mimic.isConnected).toBe(true);
|
|
180
|
+
expect(state.mimic.isReady).toBe(true);
|
|
181
|
+
expect(state.mimic.pendingCount).toBe(0);
|
|
182
|
+
expect(state.mimic.hasPendingChanges).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should preserve user state alongside mimic slice", () => {
|
|
186
|
+
const store = createStore(
|
|
187
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({
|
|
188
|
+
customField: "hello",
|
|
189
|
+
customNumber: 123,
|
|
190
|
+
}))
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const state = store.getState();
|
|
194
|
+
|
|
195
|
+
expect(state.customField).toBe("hello");
|
|
196
|
+
expect(state.customNumber).toBe(123);
|
|
197
|
+
expect(state.mimic).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should provide document reference via state.mimic.document", () => {
|
|
201
|
+
const store = createStore(
|
|
202
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const state = store.getState();
|
|
206
|
+
|
|
207
|
+
expect(state.mimic.document).toBe(mockDocument);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should reflect initial document state correctly", () => {
|
|
211
|
+
mockDocument._setState({
|
|
212
|
+
snapshot: { title: "Doc Title", count: 99 },
|
|
213
|
+
isConnected: false,
|
|
214
|
+
isReady: false,
|
|
215
|
+
pendingCount: 3,
|
|
216
|
+
hasPendingChanges: true,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const store = createStore(
|
|
220
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const state = store.getState();
|
|
224
|
+
|
|
225
|
+
expect(state.mimic.snapshot).toEqual({ title: "Doc Title", count: 99 });
|
|
226
|
+
expect(state.mimic.isConnected).toBe(false);
|
|
227
|
+
expect(state.mimic.isReady).toBe(false);
|
|
228
|
+
expect(state.mimic.pendingCount).toBe(3);
|
|
229
|
+
expect(state.mimic.hasPendingChanges).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("Reactive Updates", () => {
|
|
234
|
+
it("should update store when onStateChange fires", () => {
|
|
235
|
+
mockDocument._setState({
|
|
236
|
+
snapshot: { title: "Before", count: 1 },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const store = createStore(
|
|
240
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(store.getState().mimic.snapshot).toEqual({ title: "Before", count: 1 });
|
|
244
|
+
|
|
245
|
+
// Update document state and trigger change
|
|
246
|
+
mockDocument._setState({
|
|
247
|
+
snapshot: { title: "After", count: 2 },
|
|
248
|
+
});
|
|
249
|
+
mockDocument._triggerStateChange();
|
|
250
|
+
|
|
251
|
+
expect(store.getState().mimic.snapshot).toEqual({ title: "After", count: 2 });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should update store when onConnectionChange fires", () => {
|
|
255
|
+
mockDocument._setState({
|
|
256
|
+
isConnected: false,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const store = createStore(
|
|
260
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(store.getState().mimic.isConnected).toBe(false);
|
|
264
|
+
|
|
265
|
+
// Update connection status and trigger change
|
|
266
|
+
mockDocument._setState({
|
|
267
|
+
isConnected: true,
|
|
268
|
+
});
|
|
269
|
+
mockDocument._triggerConnectionChange();
|
|
270
|
+
|
|
271
|
+
expect(store.getState().mimic.isConnected).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should update store when onReady fires", () => {
|
|
275
|
+
mockDocument._setState({
|
|
276
|
+
isReady: false,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const store = createStore(
|
|
280
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(store.getState().mimic.isReady).toBe(false);
|
|
284
|
+
|
|
285
|
+
// Update ready status and trigger change
|
|
286
|
+
mockDocument._setState({
|
|
287
|
+
isReady: true,
|
|
288
|
+
});
|
|
289
|
+
mockDocument._triggerReady();
|
|
290
|
+
|
|
291
|
+
expect(store.getState().mimic.isReady).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should update pendingCount and hasPendingChanges on state change", () => {
|
|
295
|
+
mockDocument._setState({
|
|
296
|
+
pendingCount: 0,
|
|
297
|
+
hasPendingChanges: false,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const store = createStore(
|
|
301
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(store.getState().mimic.pendingCount).toBe(0);
|
|
305
|
+
expect(store.getState().mimic.hasPendingChanges).toBe(false);
|
|
306
|
+
|
|
307
|
+
// Simulate pending transactions
|
|
308
|
+
mockDocument._setState({
|
|
309
|
+
pendingCount: 5,
|
|
310
|
+
hasPendingChanges: true,
|
|
311
|
+
});
|
|
312
|
+
mockDocument._triggerStateChange();
|
|
313
|
+
|
|
314
|
+
expect(store.getState().mimic.pendingCount).toBe(5);
|
|
315
|
+
expect(store.getState().mimic.hasPendingChanges).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should update all mimic properties on any event", () => {
|
|
319
|
+
mockDocument._setState({
|
|
320
|
+
snapshot: { title: "V1", count: 1 },
|
|
321
|
+
isConnected: false,
|
|
322
|
+
isReady: false,
|
|
323
|
+
pendingCount: 0,
|
|
324
|
+
hasPendingChanges: false,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const store = createStore(
|
|
328
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Update all state at once
|
|
332
|
+
mockDocument._setState({
|
|
333
|
+
snapshot: { title: "V2", count: 2 },
|
|
334
|
+
isConnected: true,
|
|
335
|
+
isReady: true,
|
|
336
|
+
pendingCount: 3,
|
|
337
|
+
hasPendingChanges: true,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Triggering any event should refresh all mimic state
|
|
341
|
+
mockDocument._triggerConnectionChange();
|
|
342
|
+
|
|
343
|
+
const state = store.getState().mimic;
|
|
344
|
+
expect(state.snapshot).toEqual({ title: "V2", count: 2 });
|
|
345
|
+
expect(state.isConnected).toBe(true);
|
|
346
|
+
expect(state.isReady).toBe(true);
|
|
347
|
+
expect(state.pendingCount).toBe(3);
|
|
348
|
+
expect(state.hasPendingChanges).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("Presence", () => {
|
|
353
|
+
it("should expose reactive self + others presence when enabled", () => {
|
|
354
|
+
mockDocument = createMockClientDocument({
|
|
355
|
+
presenceEnabled: true,
|
|
356
|
+
presenceSelfId: "self",
|
|
357
|
+
presenceSelf: { name: "me" },
|
|
358
|
+
presenceOthers: new Map([
|
|
359
|
+
["other-1", { data: { name: "alice" }, userId: "u1" }],
|
|
360
|
+
]),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const store = createStore(
|
|
364
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const presence = (store.getState().mimic as any).presence;
|
|
368
|
+
expect(presence).toBeDefined();
|
|
369
|
+
expect(presence.selfId).toBe("self");
|
|
370
|
+
expect(presence.self).toEqual({ name: "me" });
|
|
371
|
+
expect(presence.others).toBeInstanceOf(Map);
|
|
372
|
+
expect(presence.all).toBeInstanceOf(Map);
|
|
373
|
+
expect(presence.others.get("other-1")).toEqual({ data: { name: "alice" }, userId: "u1" });
|
|
374
|
+
expect(presence.all.get("self")).toEqual({ data: { name: "me" } });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should update store when onPresenceChange fires (and clone Map references)", () => {
|
|
378
|
+
mockDocument = createMockClientDocument({
|
|
379
|
+
presenceEnabled: true,
|
|
380
|
+
presenceSelfId: "self",
|
|
381
|
+
presenceSelf: { name: "me" },
|
|
382
|
+
presenceOthers: new Map(),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const store = createStore(
|
|
386
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const beforePresence = (store.getState().mimic as any).presence;
|
|
390
|
+
const beforeOthersRef = beforePresence.others;
|
|
391
|
+
expect(beforePresence.self).toEqual({ name: "me" });
|
|
392
|
+
expect(beforePresence.others.size).toBe(0);
|
|
393
|
+
|
|
394
|
+
// Update others + self and trigger presence change
|
|
395
|
+
mockDocument._setState({
|
|
396
|
+
presenceSelf: { name: "me-2" },
|
|
397
|
+
presenceOthers: new Map([
|
|
398
|
+
["other-1", { data: { name: "alice" }, userId: "u1" }],
|
|
399
|
+
]),
|
|
400
|
+
});
|
|
401
|
+
mockDocument._triggerPresenceChange();
|
|
402
|
+
|
|
403
|
+
const afterPresence = (store.getState().mimic as any).presence;
|
|
404
|
+
expect(afterPresence.self).toEqual({ name: "me-2" });
|
|
405
|
+
expect(afterPresence.others.get("other-1")).toEqual({ data: { name: "alice" }, userId: "u1" });
|
|
406
|
+
|
|
407
|
+
// Critical: maps should be new references so selectors re-render
|
|
408
|
+
expect(afterPresence.others).not.toBe(beforeOthersRef);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe("Options", () => {
|
|
413
|
+
it("should subscribe to document events by default (autoSubscribe: true)", () => {
|
|
414
|
+
const store = createStore(
|
|
415
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(mockDocument._getSubscriberCount()).toBe(1);
|
|
419
|
+
|
|
420
|
+
// Verify subscription is active by updating and triggering
|
|
421
|
+
mockDocument._setState({ snapshot: { title: "Updated", count: 10 } });
|
|
422
|
+
mockDocument._triggerStateChange();
|
|
423
|
+
|
|
424
|
+
expect(store.getState().mimic.snapshot).toEqual({ title: "Updated", count: 10 });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should subscribe to document events when autoSubscribe: true explicitly", () => {
|
|
428
|
+
const store = createStore(
|
|
429
|
+
mimic(
|
|
430
|
+
mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>,
|
|
431
|
+
() => ({}),
|
|
432
|
+
{ autoSubscribe: true }
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
expect(mockDocument._getSubscriberCount()).toBe(1);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("should not subscribe to document events when autoSubscribe: false", () => {
|
|
440
|
+
const store = createStore(
|
|
441
|
+
mimic(
|
|
442
|
+
mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>,
|
|
443
|
+
() => ({}),
|
|
444
|
+
{ autoSubscribe: false }
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
expect(mockDocument._getSubscriberCount()).toBe(0);
|
|
449
|
+
|
|
450
|
+
// Verify store doesn't update when events fire
|
|
451
|
+
const initialSnapshot = store.getState().mimic.snapshot;
|
|
452
|
+
mockDocument._setState({ snapshot: { title: "Should Not Update", count: 999 } });
|
|
453
|
+
mockDocument._triggerStateChange();
|
|
454
|
+
|
|
455
|
+
expect(store.getState().mimic.snapshot).toEqual(initialSnapshot);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("User State Integration", () => {
|
|
460
|
+
it("should allow user state to use set function", () => {
|
|
461
|
+
interface UserState {
|
|
462
|
+
localCount: number;
|
|
463
|
+
increment: () => void;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const store = createStore(
|
|
467
|
+
mimic<TestSchema, undefined, UserState>(
|
|
468
|
+
mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>,
|
|
469
|
+
(set) => ({
|
|
470
|
+
localCount: 0,
|
|
471
|
+
increment: () => set((state) => ({ ...state, localCount: state.localCount + 1 })),
|
|
472
|
+
})
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(store.getState().localCount).toBe(0);
|
|
477
|
+
|
|
478
|
+
store.getState().increment();
|
|
479
|
+
|
|
480
|
+
expect(store.getState().localCount).toBe(1);
|
|
481
|
+
// Mimic state should still be present
|
|
482
|
+
expect(store.getState().mimic).toBeDefined();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("should allow user state to use get function", () => {
|
|
486
|
+
interface UserState {
|
|
487
|
+
localCount: number;
|
|
488
|
+
doubleCount: () => number;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const store = createStore(
|
|
492
|
+
mimic<TestSchema, undefined, UserState>(
|
|
493
|
+
mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>,
|
|
494
|
+
(set, get) => ({
|
|
495
|
+
localCount: 5,
|
|
496
|
+
doubleCount: () => get().localCount * 2,
|
|
497
|
+
})
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
expect(store.getState().doubleCount()).toBe(10);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("should preserve mimic state when user state changes", () => {
|
|
505
|
+
mockDocument._setState({
|
|
506
|
+
snapshot: { title: "Preserved", count: 42 },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
interface UserState {
|
|
510
|
+
value: string;
|
|
511
|
+
setValue: (v: string) => void;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const store = createStore(
|
|
515
|
+
mimic<TestSchema, undefined, UserState>(
|
|
516
|
+
mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>,
|
|
517
|
+
(set) => ({
|
|
518
|
+
value: "initial",
|
|
519
|
+
setValue: (v: string) => set((state) => ({ ...state, value: v })),
|
|
520
|
+
})
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
expect(store.getState().mimic.snapshot).toEqual({ title: "Preserved", count: 42 });
|
|
525
|
+
|
|
526
|
+
store.getState().setValue("updated");
|
|
527
|
+
|
|
528
|
+
expect(store.getState().value).toBe("updated");
|
|
529
|
+
expect(store.getState().mimic.snapshot).toEqual({ title: "Preserved", count: 42 });
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe("Multiple Events", () => {
|
|
534
|
+
it("should handle rapid successive events correctly", () => {
|
|
535
|
+
const store = createStore(
|
|
536
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Fire multiple events in quick succession
|
|
540
|
+
for (let i = 0; i < 10; i++) {
|
|
541
|
+
mockDocument._setState({ snapshot: { title: `Update ${i}`, count: i } });
|
|
542
|
+
mockDocument._triggerStateChange();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
expect(store.getState().mimic.snapshot).toEqual({ title: "Update 9", count: 9 });
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("should handle interleaved event types", () => {
|
|
549
|
+
const store = createStore(
|
|
550
|
+
mimic(mockDocument as unknown as ClientDocument.ClientDocument<TestSchema>, () => ({}))
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
mockDocument._setState({ isConnected: true });
|
|
554
|
+
mockDocument._triggerConnectionChange();
|
|
555
|
+
|
|
556
|
+
mockDocument._setState({ snapshot: { title: "New", count: 1 } });
|
|
557
|
+
mockDocument._triggerStateChange();
|
|
558
|
+
|
|
559
|
+
mockDocument._setState({ isReady: true });
|
|
560
|
+
mockDocument._triggerReady();
|
|
561
|
+
|
|
562
|
+
const state = store.getState().mimic;
|
|
563
|
+
expect(state.isConnected).toBe(true);
|
|
564
|
+
expect(state.snapshot).toEqual({ title: "New", count: 1 });
|
|
565
|
+
expect(state.isReady).toBe(true);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// =============================================================================
|
|
571
|
+
// Type Export Tests (compile-time verification)
|
|
572
|
+
// =============================================================================
|
|
573
|
+
|
|
574
|
+
describe("Type Exports", () => {
|
|
575
|
+
it("should export all expected types from index", async () => {
|
|
576
|
+
// This test verifies that the types are properly exported
|
|
577
|
+
// by importing them - if they're missing, TypeScript will error
|
|
578
|
+
const exports = await import("../../src/zustand/index");
|
|
579
|
+
|
|
580
|
+
expect(exports.mimic).toBeDefined();
|
|
581
|
+
// Type exports are verified at compile time - if this file compiles,
|
|
582
|
+
// the types are exported correctly
|
|
583
|
+
});
|
|
584
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "Preserve",
|
|
4
|
+
"lib": ["es2022", "dom", "dom.iterable"],
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"declarationDir": "dist",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"strictNullChecks": true,
|
|
12
|
+
"noUnusedLocals": false,
|
|
13
|
+
"noUnusedParameters": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
20
|
+
"noImplicitOverride": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"],
|
|
23
|
+
"exclude": ["test", "**/*.test.ts", "**/*.test.tsx", "__tests__"]
|
|
24
|
+
}
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from "tsdown";
|
|
2
|
+
|
|
3
|
+
export const input = ["./src/index.ts"];
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
target: ["es2017"],
|
|
7
|
+
entry: input,
|
|
8
|
+
dts: {
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
tsconfig: "./tsconfig.build.json",
|
|
11
|
+
},
|
|
12
|
+
// unbundle: true,
|
|
13
|
+
format: ["cjs", "esm"],
|
|
14
|
+
outExtensions: (ctx) => ({
|
|
15
|
+
dts: ctx.format === "cjs" ? ".d.cts" : ".d.mts",
|
|
16
|
+
js: ctx.format === "cjs" ? ".cjs" : ".mjs",
|
|
17
|
+
}),
|
|
18
|
+
});
|
package/vitest.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [tsconfigPaths()],
|
|
6
|
+
test: {
|
|
7
|
+
include: ["./**/*.test.ts"],
|
|
8
|
+
exclude: ["./node_modules/**"],
|
|
9
|
+
reporters: ["verbose"],
|
|
10
|
+
},
|
|
11
|
+
});
|