@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.
- package/dist/base/step.d.ts +5 -0
- package/dist/base/step.d.ts.map +1 -1
- package/dist/base/store.d.ts +317 -0
- package/dist/base/store.d.ts.map +1 -0
- package/dist/base/store.test.d.ts +2 -0
- package/dist/base/store.test.d.ts.map +1 -0
- package/dist/base/workflow.d.ts +2 -0
- package/dist/base/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +403 -3
- package/dist/lib/workflow-runner.d.ts +3 -1
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/utils/map.d.ts +1 -1
- package/dist/utils/map.d.ts.map +1 -1
- package/package.json +13 -3
- package/src/base/step.ts +8 -0
- package/src/base/store.test.ts +176 -0
- package/src/base/store.ts +833 -0
- package/src/base/workflow.ts +2 -0
- package/src/index.ts +37 -0
- package/src/lib/workflow-runner.ts +14 -2
- package/src/utils/map.test.ts +1 -1
- package/src/utils/map.ts +8 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/base/map.d.ts +0 -21
- package/dist/base/map.d.ts.map +0 -1
- package/dist/base/map.test.d.ts +0 -2
- package/dist/base/map.test.d.ts.map +0 -1
- package/dist/base/schedule.d.ts +0 -71
- package/dist/base/schedule.d.ts.map +0 -1
- package/dist/base/types.d.ts +0 -24
- package/dist/base/types.d.ts.map +0 -1
|
@@ -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
|
+
});
|