Skip to content

Commit 3aa6d56

Browse files
committed
add DRAFT migration guide
1 parent fa5768d commit 3aa6d56

File tree

2 files changed

+229
-1
lines changed

2 files changed

+229
-1
lines changed

docs/structure.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
["typescript.mdx", "TypeScript"],
55
["thinking-in-effection.mdx", "Thinking in Effection"],
66
["async-rosetta-stone.mdx", "Async Rosetta Stone"],
7-
["tutorial.mdx", "Tutorial"]
7+
["tutorial.mdx", "Tutorial"],
8+
["upgrade", "Upgrading from V3"]
89
],
910
"Learn Effection": [
1011
["operations.mdx", "Operations"],

docs/upgrade.mdx

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# Upgrading from Effection version 3
2+
3+
For the most part, the changes between Effection versions 3 and 4 are
4+
internal, and while the public API remains largely untouched , there
5+
were some places where it was appropriate to make breaking changes.
6+
However, in the cases where we did, it was guided by our simple
7+
principle: to _embrace_ JavaScript, not fight against it. We asked
8+
ourselvers: How can we make Effection APIs feel even _more_ natural
9+
and familiar to JavaScript developers while providing "just enough"
10+
functionality to bring all the wonderful benefits of structured
11+
concurrency and effects to your applications.
12+
13+
Among other things, this included the refinement of several key
14+
functions that just did too much in v3. These changes make Effection
15+
harmonize even more with the greate JavaScript ecosystem, but they do
16+
require some attention during migration.
17+
18+
## Simplifying `call()`
19+
20+
In Effection, few APIs were as versatile as the [v3 `call()`
21+
function][v3-call]. It could invoke functions, evaluate promises,
22+
treat constants as operations, establish error boundaries, and even
23+
manage concurrency to boot. But one piece of feedback that we
24+
consistently got was "how does this relate to
25+
`Function.prototype.call()." Sadly, the answer was: only tangentially.
26+
27+
So in order to make using `call()` require no learning beyond how its
28+
vanilla counterpart works, we've simplified it to what you would
29+
expect from something named "call". It invokes functions as
30+
operations, and that's it.
31+
32+
When you're upgrading promise-based code, you'll now use the `until()` helper, which converts a promise into an operation:
33+
34+
```js
35+
let response = yield* call(fetch("https://frontside.com/effection"));
36+
```
37+
38+
To do this in v4, use the [`until()`][until] utility function
39+
40+
```js
41+
let response = yield* until(fetch("https://frontside.com/effection"));
42+
```
43+
44+
`v3` call also allowed for the rare cases where you need to evaluate a constant value as an operation.
45+
46+
```js
47+
let five = yield* call(5);
48+
```
49+
50+
Now however, there's now a dedicated `constant()` helper:
51+
52+
```js
53+
let five = yield* constant(5);
54+
```
55+
56+
`call()` could also be used in v3 to delimit a concurrency boundary which would terminate all children within its scope
57+
58+
The most significant change involves how called operations are
59+
delimited. In v3, `call()` automatically established both error
60+
boundaries and concurrency boundaries around its body, meaning any
61+
tasks spawned within a `call()` would be automatically cleaned up when
62+
the call completed. In v4, `call()` no longer does this—it simply
63+
invokes the function and returns its result.
64+
65+
```js
66+
// v4 - call() does NOT establish boundaries
67+
yield* call(function*() {
68+
// spawned tasks here are NOT terminated before call returns its value
69+
yield* spawn(someBackgroundTask);
70+
return "done";
71+
});
72+
```
73+
74+
If you need those boundaries—and you often will—use the new `scoped()` function:
75+
76+
```js
77+
// v4 - scoped() establishes boundaries
78+
yield* scoped(function*() {
79+
// spawned tasks here ARE terminated before the scoped body returns
80+
yield* spawn(someBackgroundTask);
81+
return "done";
82+
});
83+
```
84+
85+
## Rethinking `action()`
86+
87+
The `action()` function has undergone a similar simplification. In our documentation, we describe `action()` as Effection's equivalent to `new Promise()`, and v4 makes this analogy much stronger.
88+
89+
In v3, an action is written using an operation. For example, consider this implementation of a `sleep()` operation:
90+
91+
```js
92+
// v3 operation function
93+
function sleep(milliseconds) {
94+
return action(function*(resolve) {
95+
let timeout = setTimeout(resolve, milliseconds);
96+
try {
97+
yield* suspend();
98+
} finally {
99+
clearTimeout(timeout);
100+
}
101+
});
102+
}
103+
```
104+
105+
The v4 equivalent looks much more like a Promise constructor:
106+
107+
```js
108+
// v4 - action strongly resembles Promise
109+
function sleep(milliseconds) {
110+
return action((resolve, reject) => {
111+
let timeout = setTimeout(resolve, milliseconds);
112+
return () => { clearTimeout(timeout); }
113+
});
114+
}
115+
```
116+
117+
Notice how the v4 version takes a synchronous function that receives
118+
both `resolve` and `reject` callbacks, just like `new Promise()`.
119+
Instead of using `try/finally` blocks for teardown, you just return a
120+
synchronous cleanup function.
121+
122+
Like the simplified `call()`, actions in v4 no longer establish their
123+
own concurrency or error boundaries. If you need boundaries around
124+
action usage, wrap the call with `scoped()`.
125+
126+
## Task Execution Priority
127+
128+
The most subtle but important change in v4 involves how tasks are
129+
scheduled for execution. This change won't trigger any deprecation
130+
warnings, but it can affect the behavior of your applications in ways
131+
that might not be immediately obvious.
132+
133+
In v3, tasks ran immediately whenever an event came in that caused it
134+
to resume. A child task spawned in the background would start
135+
executing immediately, even while its parent was still running
136+
synchronous code. v4 changes this: a parent task always has priority
137+
over its children.
138+
139+
Consider this example:
140+
141+
```js
142+
await run(function* example() {
143+
console.log('parent: start');
144+
yield* spawn(function*() {
145+
console.log('child: start');
146+
yield* sleep(10);
147+
console.log('child: end');
148+
});
149+
console.log('parent: middle');
150+
// Lots of synchronous work here
151+
for (let i = 0; i < 1000; i++) {
152+
// The child won't run during this loop in v4
153+
}
154+
console.log('parent: before async');
155+
yield* sleep(100); // This is when the child finally gets to run
156+
console.log('parent: end');
157+
})
158+
```
159+
160+
In v3, you would see output like this:
161+
162+
```
163+
parent: start
164+
child: start
165+
parent: middle
166+
parent: before async
167+
parent: end
168+
child: end
169+
```
170+
171+
But in v4, the output changes to:
172+
173+
```
174+
parent: start
175+
parent: middle
176+
parent: before async
177+
child: start
178+
parent: end
179+
child: end
180+
```
181+
182+
The key difference is that the child task doesn't get to run until the
183+
parent yields control to a truly asynchronous operation—in this case,
184+
`sleep(1)`. Purely synchronous operations, even when wrapped with
185+
`yield* call()`, won't give child tasks a chance to execute. Furthermore, in cases where a single event schedules multiple tasks to resume, the parent task will resume first.
186+
187+
This change makes task execution more predictable and allows parents
188+
to always have the necessary priority required to supervise the
189+
execution of their children. However, it does mean that if your v3
190+
code relied on child tasks starting immediately, you might need to
191+
adjust your approach.
192+
193+
## Migration Strategies
194+
195+
Most of the changes from v3 to v4 will be caught by deprecation
196+
warnings, making the upgrade process straightforward. The `call()` and
197+
`action()` changes are mechanical—update the syntax, import the new
198+
helpers, and you're done.
199+
200+
The task execution priority change requires more thought. If your
201+
application has timing-sensitive code where child tasks need to start
202+
immediately, you have several approaches:
203+
204+
You can restructure your parent tasks to yield control explicitly by
205+
adding asynchronous operations where needed. Even `yield* sleep(0)`
206+
will give child tasks a chance to start:
207+
208+
```js
209+
function* parent() {
210+
yield* spawn(childTask);
211+
yield* sleep(0); // Yields control to child immediately
212+
// Continue with parent work
213+
}
214+
```
215+
216+
You can also reconsider your task hierarchy. Sometimes what you
217+
thought needed to be a parent-child relationship might work better as
218+
sibling tasks within a shared scope.
219+
220+
221+
If you encounter issues during upgrade, please file an
222+
[issue](https://github.com/thefrontside/effection/issues/new). Not
223+
only will that allow us to lend a hand, but it will also give us an
224+
opportunity to improve this migration guide!
225+
226+
[v3-call]: https://frontside.com/effection/api/v3/call/
227+
[until]: https://frontside.com/effection/api/v4/until/

0 commit comments

Comments
 (0)