@yieldstar/core 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ import { expect, test } from "vitest";
2
+ import {
3
+ cloneStoreState,
4
+ storePathsIntersect,
5
+ trackStoreSelector,
6
+ trackStoreUpdater,
7
+ unwrapTrackedValue,
8
+ type StorePath,
9
+ } from "./store";
10
+
11
+ type State = {
12
+ messages: { id: string; processed?: boolean; claimedBy?: string }[];
13
+ status: string;
14
+ profile: { name: string; tags: Record<string, string> };
15
+ };
16
+
17
+ function makeState(): State {
18
+ return {
19
+ messages: [
20
+ { id: "msg-1" },
21
+ { id: "msg-2" },
22
+ { id: "msg-3" },
23
+ { id: "msg-4" },
24
+ ],
25
+ status: "idle",
26
+ profile: { name: "dan", tags: {} },
27
+ };
28
+ }
29
+
30
+ function wakes(readPath: StorePath, writePaths: StorePath[]): boolean {
31
+ return storePathsIntersect([readPath], writePaths);
32
+ }
33
+
34
+ test("trackStoreUpdater: push onto an array records index paths that wake array waiters", () => {
35
+ const draft = makeState();
36
+ const tracked = trackStoreUpdater(draft);
37
+
38
+ tracked.draft.messages.push({ id: "msg-5" });
39
+
40
+ const paths = tracked.writePaths();
41
+ expect(paths).toContainEqual(["messages", 4]);
42
+ // A waiter on ["messages"] wakes via prefix intersection
43
+ expect(wakes(["messages"], paths)).toBe(true);
44
+ // The mutation landed on the raw draft
45
+ expect(draft.messages).toHaveLength(5);
46
+ expect(draft.messages[4]).toEqual({ id: "msg-5" });
47
+ });
48
+
49
+ test("trackStoreUpdater: splice and shift record the shifted indices", () => {
50
+ const draft = makeState();
51
+ const tracked = trackStoreUpdater(draft);
52
+
53
+ tracked.draft.messages.splice(1, 1);
54
+
55
+ const paths = tracked.writePaths();
56
+ // Elements shift down: indices 1..2 rewritten, index 3 deleted
57
+ expect(wakes(["messages", 1], paths)).toBe(true);
58
+ expect(wakes(["messages", 2], paths)).toBe(true);
59
+ expect(wakes(["messages", 3], paths)).toBe(true);
60
+ expect(draft.messages.map((m) => m.id)).toEqual(["msg-1", "msg-3", "msg-4"]);
61
+
62
+ const shifted = trackStoreUpdater(makeState());
63
+ shifted.draft.messages.shift();
64
+ const shiftPaths = shifted.writePaths();
65
+ expect(wakes(["messages", 0], shiftPaths)).toBe(true);
66
+ expect(wakes(["messages", 2], shiftPaths)).toBe(true);
67
+ });
68
+
69
+ test("trackStoreUpdater: nested field set records the full path", () => {
70
+ const draft = makeState();
71
+ const tracked = trackStoreUpdater(draft);
72
+
73
+ tracked.draft.profile.name = "grant";
74
+ tracked.draft.messages[0]!.processed = true;
75
+
76
+ const paths = tracked.writePaths();
77
+ expect(paths).toContainEqual(["profile", "name"]);
78
+ expect(paths).toContainEqual(["messages", 0, "processed"]);
79
+ // Unrelated paths do not intersect
80
+ expect(wakes(["status"], paths)).toBe(false);
81
+ expect(draft.profile.name).toBe("grant");
82
+ expect(draft.messages[0]!.processed).toBe(true);
83
+ });
84
+
85
+ test("trackStoreUpdater: Object.assign onto a nested object records each assigned key", () => {
86
+ const draft = makeState();
87
+ const tracked = trackStoreUpdater(draft);
88
+
89
+ Object.assign(tracked.draft.profile.tags, { a: "1", b: "2" });
90
+
91
+ const paths = tracked.writePaths();
92
+ expect(paths).toContainEqual(["profile", "tags", "a"]);
93
+ expect(paths).toContainEqual(["profile", "tags", "b"]);
94
+ expect(draft.profile.tags).toEqual({ a: "1", b: "2" });
95
+ });
96
+
97
+ test("trackStoreUpdater: delete records the deleted path", () => {
98
+ const draft = makeState();
99
+ draft.profile.tags = { a: "1" };
100
+ const tracked = trackStoreUpdater(draft);
101
+
102
+ delete tracked.draft.profile.tags["a"];
103
+
104
+ expect(tracked.writePaths()).toContainEqual(["profile", "tags", "a"]);
105
+ expect(draft.profile.tags).toEqual({});
106
+ });
107
+
108
+ test("trackStoreUpdater: defineProperty records conservatively", () => {
109
+ const draft = makeState();
110
+ const tracked = trackStoreUpdater(draft);
111
+
112
+ Object.defineProperty(tracked.draft.profile, "name", {
113
+ value: "defined",
114
+ writable: true,
115
+ enumerable: true,
116
+ configurable: true,
117
+ });
118
+
119
+ expect(tracked.writePaths()).toContainEqual(["profile", "name"]);
120
+ expect(draft.profile.name).toBe("defined");
121
+ });
122
+
123
+ test("trackStoreUpdater: duplicate write paths are collapsed", () => {
124
+ const draft = makeState();
125
+ const tracked = trackStoreUpdater(draft);
126
+
127
+ tracked.draft.status = "working";
128
+ tracked.draft.status = "idle";
129
+ tracked.draft.status = "working";
130
+
131
+ expect(
132
+ tracked.writePaths().filter((p) => p.join(".") === "status")
133
+ ).toHaveLength(1);
134
+ });
135
+
136
+ test("trackStoreUpdater: values assigned through the proxy are unwrapped onto the raw draft", () => {
137
+ const draft = makeState();
138
+ const tracked = trackStoreUpdater(draft);
139
+
140
+ // Read a child (returns a proxy) and store it elsewhere in the draft –
141
+ // the raw draft must receive the raw object, not the proxy wrapper.
142
+ const first = tracked.draft.messages[0]!;
143
+ (tracked.draft as any).lastMessage = first;
144
+
145
+ const stored = (draft as any).lastMessage;
146
+ expect(stored).toBe(draft.messages[0]);
147
+ expect(unwrapTrackedValue(stored)).toBe(stored);
148
+ // Proxy-free: structuredClone rejects proxies, so this must not throw
149
+ expect(() => structuredClone(draft)).not.toThrow();
150
+ });
151
+
152
+ test("trackStoreUpdater: mutations through a value selected via trackStoreSelector are recorded", () => {
153
+ const draft = makeState();
154
+ const tracked = trackStoreUpdater(draft);
155
+
156
+ const { result } = trackStoreSelector(tracked.draft, (s) =>
157
+ s.messages.find((m) => !m.claimedBy)
158
+ );
159
+ const selected = unwrapTrackedValue(result)!;
160
+ selected.claimedBy = "exec-1";
161
+
162
+ expect(tracked.writePaths()).toContainEqual(["messages", 0, "claimedBy"]);
163
+ expect(draft.messages[0]!.claimedBy).toBe("exec-1");
164
+ });
165
+
166
+ test("cloneStoreState launders proxy wrappers into plain objects", () => {
167
+ const tracked = trackStoreUpdater(makeState());
168
+ // A fresh object holding a nested proxy (the shallow set-unwrap cannot
169
+ // reach it) – commit-time cloning must still produce a proxy-free state.
170
+ const dirty = { wrapper: { inner: tracked.draft.messages } };
171
+
172
+ const clean = cloneStoreState(dirty);
173
+
174
+ expect(() => structuredClone(clean)).not.toThrow();
175
+ expect(clean.wrapper.inner).toEqual(makeState().messages);
176
+ });