svelte 5.46.1 → 5.46.4

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svelte",
3
3
  "description": "Cybernetically enhanced web apps",
4
4
  "license": "MIT",
5
- "version": "5.46.1",
5
+ "version": "5.46.4",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -163,7 +163,7 @@
163
163
  "aria-query": "^5.3.1",
164
164
  "axobject-query": "^4.1.0",
165
165
  "clsx": "^2.1.1",
166
- "devalue": "^5.5.0",
166
+ "devalue": "^5.6.2",
167
167
  "esm-env": "^1.2.1",
168
168
  "esrap": "^2.2.1",
169
169
  "is-reference": "^3.0.3",
@@ -6,6 +6,7 @@ import { regex_not_newline_characters } from '../../patterns.js';
6
6
  import * as e from '../../../errors.js';
7
7
  import * as w from '../../../warnings.js';
8
8
  import { is_text_attribute } from '../../../utils/ast.js';
9
+ import { locator } from '../../../state.js';
9
10
 
10
11
  const regex_closing_script_tag = /<\/script\s*>/;
11
12
  const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
@@ -39,9 +40,15 @@ export function read_script(parser, start, attributes) {
39
40
  parser.acorn_error(err);
40
41
  }
41
42
 
42
- // TODO is this necessary?
43
43
  ast.start = script_start;
44
44
 
45
+ if (ast.loc) {
46
+ // Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
47
+ // we need it to be the start of the `<script>` contents
48
+ ({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
49
+ ({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
50
+ }
51
+
45
52
  /** @type {'default' | 'module'} */
46
53
  let context = 'default';
47
54
 
@@ -657,7 +657,8 @@ export function analyze_component(root, source, options) {
657
657
  if (
658
658
  binding &&
659
659
  binding.kind === 'normal' &&
660
- binding.declaration_kind !== 'import'
660
+ binding.declaration_kind !== 'import' &&
661
+ binding.declaration_kind !== 'function'
661
662
  ) {
662
663
  binding.kind = 'state';
663
664
  binding.mutated = true;
@@ -54,7 +54,9 @@ export function EachBlock(node, context) {
54
54
 
55
55
  // collect transitive dependencies...
56
56
  for (const binding of node.metadata.expression.dependencies) {
57
- collect_transitive_dependencies(binding, node.metadata.transitive_deps);
57
+ if (binding.declaration_kind !== 'function') {
58
+ collect_transitive_dependencies(binding, node.metadata.transitive_deps);
59
+ }
58
60
  }
59
61
 
60
62
  // ...and ensure they are marked as state, so they can be turned
@@ -209,11 +209,11 @@ export function VariableDeclaration(node, context) {
209
209
  let call = b.call(
210
210
  '$.async_derived',
211
211
  b.thunk(expression, true),
212
+ dev && b.literal(declarator.id.name),
212
213
  location ? b.literal(location) : undefined
213
214
  );
214
215
 
215
216
  call = should_save ? save(call) : b.await(call);
216
- if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
217
217
 
218
218
  declarations.push(b.declarator(declarator.id, call));
219
219
  } else {
@@ -244,17 +244,16 @@ export function VariableDeclaration(node, context) {
244
244
  call = b.call(
245
245
  '$.async_derived',
246
246
  b.thunk(expression, true),
247
+ dev &&
248
+ b.literal(
249
+ `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`
250
+ ),
247
251
  location ? b.literal(location) : undefined
248
252
  );
249
253
 
250
254
  call = should_save ? save(call) : b.await(call);
251
255
  }
252
256
 
253
- if (dev) {
254
- const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
255
- call = b.call('$.tag', call, b.literal(label));
256
- }
257
-
258
257
  declarations.push(b.declarator(id, call));
259
258
  }
260
259
 
@@ -86,7 +86,14 @@ export function transform_body(instance_body, runner, transform) {
86
86
  }
87
87
 
88
88
  if (s.node.type === 'ExpressionStatement') {
89
- const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression));
89
+ // the expression may be a $inspect call, which will be transformed into an empty statement
90
+ const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
91
+ transform(s.node.expression)
92
+ );
93
+
94
+ if (expression.type === 'EmptyStatement') {
95
+ return null;
96
+ }
90
97
 
91
98
  return expression.type === 'AwaitExpression'
92
99
  ? b.thunk(expression, true)
@@ -21,7 +21,7 @@ import { get_boundary } from './boundary.js';
21
21
  export function async(node, blockers = [], expressions = [], fn) {
22
22
  var boundary = get_boundary();
23
23
  var batch = /** @type {Batch} */ (current_batch);
24
- var blocking = !boundary.is_pending();
24
+ var blocking = boundary.is_rendered();
25
25
 
26
26
  boundary.update_pending_count(1);
27
27
  batch.increment(blocking);
@@ -2,8 +2,10 @@
2
2
  import {
3
3
  BOUNDARY_EFFECT,
4
4
  COMMENT_NODE,
5
+ DIRTY,
5
6
  EFFECT_PRESERVED,
6
- EFFECT_TRANSPARENT
7
+ EFFECT_TRANSPARENT,
8
+ MAYBE_DIRTY
7
9
  } from '#client/constants';
8
10
  import { HYDRATION_START_ELSE } from '../../../../constants.js';
9
11
  import { component_context, set_component_context } from '../../context.js';
@@ -34,11 +36,13 @@ import { queue_micro_task } from '../task.js';
34
36
  import * as e from '../../errors.js';
35
37
  import * as w from '../../warnings.js';
36
38
  import { DEV } from 'esm-env';
37
- import { Batch } from '../../reactivity/batch.js';
39
+ import { Batch, schedule_effect } from '../../reactivity/batch.js';
38
40
  import { internal_set, source } from '../../reactivity/sources.js';
39
41
  import { tag } from '../../dev/tracing.js';
40
42
  import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
41
43
  import { create_text } from '../operations.js';
44
+ import { defer_effect } from '../../reactivity/utils.js';
45
+ import { set_signal_status } from '../../reactivity/status.js';
42
46
 
43
47
  /**
44
48
  * @typedef {{
@@ -64,7 +68,7 @@ export class Boundary {
64
68
  /** @type {Boundary | null} */
65
69
  parent;
66
70
 
67
- #pending = false;
71
+ is_pending = false;
68
72
 
69
73
  /** @type {TemplateNode} */
70
74
  #anchor;
@@ -101,6 +105,12 @@ export class Boundary {
101
105
 
102
106
  #is_creating_fallback = false;
103
107
 
108
+ /** @type {Set<Effect>} */
109
+ #dirty_effects = new Set();
110
+
111
+ /** @type {Set<Effect>} */
112
+ #maybe_dirty_effects = new Set();
113
+
104
114
  /**
105
115
  * A source containing the number of pending async deriveds/expressions.
106
116
  * Only created if `$effect.pending()` is used inside the boundary,
@@ -134,7 +144,7 @@ export class Boundary {
134
144
 
135
145
  this.parent = /** @type {Effect} */ (active_effect).b;
136
146
 
137
- this.#pending = !!this.#props.pending;
147
+ this.is_pending = !!this.#props.pending;
138
148
 
139
149
  this.#effect = block(() => {
140
150
  /** @type {Effect} */ (active_effect).b = this;
@@ -151,6 +161,10 @@ export class Boundary {
151
161
  this.#hydrate_pending_content();
152
162
  } else {
153
163
  this.#hydrate_resolved_content();
164
+
165
+ if (this.#pending_count === 0) {
166
+ this.is_pending = false;
167
+ }
154
168
  }
155
169
  } else {
156
170
  var anchor = this.#get_anchor();
@@ -164,7 +178,7 @@ export class Boundary {
164
178
  if (this.#pending_count > 0) {
165
179
  this.#show_pending_snippet();
166
180
  } else {
167
- this.#pending = false;
181
+ this.is_pending = false;
168
182
  }
169
183
  }
170
184
 
@@ -184,10 +198,6 @@ export class Boundary {
184
198
  } catch (error) {
185
199
  this.error(error);
186
200
  }
187
-
188
- // Since server rendered resolved content, we never show pending state
189
- // Even if client-side async operations are still running, the content is already displayed
190
- this.#pending = false;
191
201
  }
192
202
 
193
203
  #hydrate_pending_content() {
@@ -212,7 +222,7 @@ export class Boundary {
212
222
  this.#pending_effect = null;
213
223
  });
214
224
 
215
- this.#pending = false;
225
+ this.is_pending = false;
216
226
  }
217
227
  });
218
228
  }
@@ -220,7 +230,7 @@ export class Boundary {
220
230
  #get_anchor() {
221
231
  var anchor = this.#anchor;
222
232
 
223
- if (this.#pending) {
233
+ if (this.is_pending) {
224
234
  this.#pending_anchor = create_text();
225
235
  this.#anchor.before(this.#pending_anchor);
226
236
 
@@ -231,11 +241,19 @@ export class Boundary {
231
241
  }
232
242
 
233
243
  /**
234
- * Returns `true` if the effect exists inside a boundary whose pending snippet is shown
244
+ * Defer an effect inside a pending boundary until the boundary resolves
245
+ * @param {Effect} effect
246
+ */
247
+ defer_effect(effect) {
248
+ defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects);
249
+ }
250
+
251
+ /**
252
+ * Returns `false` if the effect exists inside a boundary whose pending snippet is shown
235
253
  * @returns {boolean}
236
254
  */
237
- is_pending() {
238
- return this.#pending || (!!this.parent && this.parent.is_pending());
255
+ is_rendered() {
256
+ return !this.is_pending && (!this.parent || this.parent.is_rendered());
239
257
  }
240
258
 
241
259
  has_pending_snippet() {
@@ -298,7 +316,24 @@ export class Boundary {
298
316
  this.#pending_count += d;
299
317
 
300
318
  if (this.#pending_count === 0) {
301
- this.#pending = false;
319
+ this.is_pending = false;
320
+
321
+ // any effects that were encountered and deferred during traversal
322
+ // should be rescheduled — after the next traversal (which will happen
323
+ // immediately, due to the same update that brought us here)
324
+ // the effects will be flushed
325
+ for (const e of this.#dirty_effects) {
326
+ set_signal_status(e, DIRTY);
327
+ schedule_effect(e);
328
+ }
329
+
330
+ for (const e of this.#maybe_dirty_effects) {
331
+ set_signal_status(e, MAYBE_DIRTY);
332
+ schedule_effect(e);
333
+ }
334
+
335
+ this.#dirty_effects.clear();
336
+ this.#maybe_dirty_effects.clear();
302
337
 
303
338
  if (this.#pending_effect) {
304
339
  pause_effect(this.#pending_effect, () => {
@@ -394,7 +429,7 @@ export class Boundary {
394
429
 
395
430
  // we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
396
431
  // but it would be really weird to show the parent's boundary on a child reset.
397
- this.#pending = this.has_pending_snippet();
432
+ this.is_pending = this.has_pending_snippet();
398
433
 
399
434
  this.#main_effect = this.#run(() => {
400
435
  this.#is_creating_fallback = false;
@@ -404,7 +439,7 @@ export class Boundary {
404
439
  if (this.#pending_count > 0) {
405
440
  this.#show_pending_snippet();
406
441
  } else {
407
- this.#pending = false;
442
+ this.is_pending = false;
408
443
  }
409
444
  };
410
445
 
@@ -98,7 +98,6 @@ export {
98
98
  with_script
99
99
  } from './dom/template.js';
100
100
  export {
101
- async_body,
102
101
  for_await_track_reactivity_loss,
103
102
  run,
104
103
  save,
@@ -25,7 +25,6 @@ import {
25
25
  set_from_async_derived
26
26
  } from './deriveds.js';
27
27
  import { aborted } from './effects.js';
28
- import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.js';
29
28
 
30
29
  /**
31
30
  * @param {Array<Promise<void>>} blockers
@@ -211,51 +210,6 @@ export function unset_context() {
211
210
  }
212
211
  }
213
212
 
214
- /**
215
- * @param {TemplateNode} anchor
216
- * @param {(target: TemplateNode) => Promise<void>} fn
217
- */
218
- export async function async_body(anchor, fn) {
219
- var boundary = get_boundary();
220
- var batch = /** @type {Batch} */ (current_batch);
221
- var blocking = !boundary.is_pending();
222
-
223
- boundary.update_pending_count(1);
224
- batch.increment(blocking);
225
-
226
- var active = /** @type {Effect} */ (active_effect);
227
-
228
- var was_hydrating = hydrating;
229
- var next_hydrate_node = undefined;
230
-
231
- if (was_hydrating) {
232
- hydrate_next();
233
- next_hydrate_node = skip_nodes(false);
234
- }
235
-
236
- try {
237
- var promise = fn(anchor);
238
- } finally {
239
- if (next_hydrate_node) {
240
- set_hydrate_node(next_hydrate_node);
241
- hydrate_next();
242
- }
243
- }
244
-
245
- try {
246
- await promise;
247
- } catch (error) {
248
- if (!aborted(active)) {
249
- invoke_error_boundary(error, active);
250
- }
251
- } finally {
252
- boundary.update_pending_count(-1);
253
- batch.decrement(blocking);
254
-
255
- unset_context();
256
- }
257
- }
258
-
259
213
  /**
260
214
  * @param {Array<() => void | Promise<void>>} thunks
261
215
  */
@@ -264,7 +218,7 @@ export function run(thunks) {
264
218
 
265
219
  var boundary = get_boundary();
266
220
  var batch = /** @type {Batch} */ (current_batch);
267
- var blocking = !boundary.is_pending();
221
+ var blocking = boundary.is_rendered();
268
222
 
269
223
  boundary.update_pending_count(1);
270
224
  batch.increment(blocking);
@@ -298,17 +252,13 @@ export function run(thunks) {
298
252
  throw STALE_REACTION;
299
253
  }
300
254
 
301
- try {
302
- restore();
303
- return fn();
304
- } finally {
305
- // TODO do we need it here as well as below?
306
- unset_context();
307
- }
255
+ restore();
256
+ return fn();
308
257
  })
309
258
  .catch(handle_error)
310
259
  .finally(() => {
311
260
  unset_context();
261
+ current_batch?.deactivate();
312
262
  });
313
263
 
314
264
  promises.push(promise);
@@ -1,5 +1,6 @@
1
1
  /** @import { Fork } from 'svelte' */
2
2
  /** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
3
+ /** @import { Boundary } from '../dom/blocks/boundary' */
3
4
  import {
4
5
  BLOCK_EFFECT,
5
6
  BRANCH_EFFECT,
@@ -17,7 +18,6 @@ import {
17
18
  EAGER_EFFECT,
18
19
  HEAD_EFFECT,
19
20
  ERROR_VALUE,
20
- WAS_MARKED,
21
21
  MANAGED_EFFECT
22
22
  } from '#client/constants';
23
23
  import { async_mode_flag } from '../../flags/index.js';
@@ -25,10 +25,10 @@ import { deferred, define_property } from '../../shared/utils.js';
25
25
  import {
26
26
  active_effect,
27
27
  get,
28
+ increment_write_version,
28
29
  is_dirty,
29
30
  is_updating_effect,
30
31
  set_is_updating_effect,
31
- set_signal_status,
32
32
  update_effect
33
33
  } from '../runtime.js';
34
34
  import * as e from '../errors.js';
@@ -37,15 +37,9 @@ import { DEV } from 'esm-env';
37
37
  import { invoke_error_boundary } from '../error-handling.js';
38
38
  import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
39
39
  import { eager_effect, unlink_effect } from './effects.js';
40
-
41
- /**
42
- * @typedef {{
43
- * parent: EffectTarget | null;
44
- * effect: Effect | null;
45
- * effects: Effect[];
46
- * render_effects: Effect[];
47
- * }} EffectTarget
48
- */
40
+ import { defer_effect } from './utils.js';
41
+ import { UNINITIALIZED } from '../../../constants.js';
42
+ import { set_signal_status } from './status.js';
49
43
 
50
44
  /** @type {Set<Batch>} */
51
45
  const batches = new Set();
@@ -161,16 +155,14 @@ export class Batch {
161
155
 
162
156
  this.apply();
163
157
 
164
- /** @type {EffectTarget} */
165
- var target = {
166
- parent: null,
167
- effect: null,
168
- effects: [],
169
- render_effects: []
170
- };
158
+ /** @type {Effect[]} */
159
+ var effects = [];
160
+
161
+ /** @type {Effect[]} */
162
+ var render_effects = [];
171
163
 
172
164
  for (const root of root_effects) {
173
- this.#traverse_effect_tree(root, target);
165
+ this.#traverse_effect_tree(root, effects, render_effects);
174
166
  // Note: #traverse_effect_tree runs block effects eagerly, which can schedule effects,
175
167
  // which means queued_root_effects now may be filled again.
176
168
 
@@ -183,16 +175,16 @@ export class Batch {
183
175
  }
184
176
 
185
177
  if (this.is_deferred()) {
186
- this.#defer_effects(target.effects);
187
- this.#defer_effects(target.render_effects);
178
+ this.#defer_effects(render_effects);
179
+ this.#defer_effects(effects);
188
180
  } else {
189
181
  // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
190
182
  // newly updated sources, which could lead to infinite loops when effects run over and over again.
191
183
  previous_batch = this;
192
184
  current_batch = null;
193
185
 
194
- flush_queued_effects(target.render_effects);
195
- flush_queued_effects(target.effects);
186
+ flush_queued_effects(render_effects);
187
+ flush_queued_effects(effects);
196
188
 
197
189
  previous_batch = null;
198
190
 
@@ -206,13 +198,17 @@ export class Batch {
206
198
  * Traverse the effect tree, executing effects or stashing
207
199
  * them for later execution as appropriate
208
200
  * @param {Effect} root
209
- * @param {EffectTarget} target
201
+ * @param {Effect[]} effects
202
+ * @param {Effect[]} render_effects
210
203
  */
211
- #traverse_effect_tree(root, target) {
204
+ #traverse_effect_tree(root, effects, render_effects) {
212
205
  root.f ^= CLEAN;
213
206
 
214
207
  var effect = root.first;
215
208
 
209
+ /** @type {Effect | null} */
210
+ var pending_boundary = null;
211
+
216
212
  while (effect !== null) {
217
213
  var flags = effect.f;
218
214
  var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
@@ -220,24 +216,32 @@ export class Batch {
220
216
 
221
217
  var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
222
218
 
223
- if ((effect.f & BOUNDARY_EFFECT) !== 0 && effect.b?.is_pending()) {
224
- target = {
225
- parent: target,
226
- effect,
227
- effects: [],
228
- render_effects: []
229
- };
219
+ // Inside a `<svelte:boundary>` with a pending snippet,
220
+ // all effects are deferred until the boundary resolves
221
+ // (except block/async effects, which run immediately)
222
+ if (
223
+ async_mode_flag &&
224
+ pending_boundary === null &&
225
+ (flags & BOUNDARY_EFFECT) !== 0 &&
226
+ effect.b?.is_pending
227
+ ) {
228
+ pending_boundary = effect;
230
229
  }
231
230
 
232
231
  if (!skip && effect.fn !== null) {
233
232
  if (is_branch) {
234
233
  effect.f ^= CLEAN;
234
+ } else if (
235
+ pending_boundary !== null &&
236
+ (flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0
237
+ ) {
238
+ /** @type {Boundary} */ (pending_boundary.b).defer_effect(effect);
235
239
  } else if ((flags & EFFECT) !== 0) {
236
- target.effects.push(effect);
240
+ effects.push(effect);
237
241
  } else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
238
- target.render_effects.push(effect);
242
+ render_effects.push(effect);
239
243
  } else if (is_dirty(effect)) {
240
- if ((effect.f & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
244
+ if ((flags & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
241
245
  update_effect(effect);
242
246
  }
243
247
 
@@ -253,14 +257,8 @@ export class Batch {
253
257
  effect = effect.next;
254
258
 
255
259
  while (effect === null && parent !== null) {
256
- if (parent === target.effect) {
257
- // TODO rather than traversing into pending boundaries and deferring the effects,
258
- // could we just attach the effects _to_ the pending boundary and schedule them
259
- // once the boundary is ready?
260
- this.#defer_effects(target.effects);
261
- this.#defer_effects(target.render_effects);
262
-
263
- target = /** @type {EffectTarget} */ (target.parent);
260
+ if (parent === pending_boundary) {
261
+ pending_boundary = null;
264
262
  }
265
263
 
266
264
  effect = parent.next;
@@ -273,36 +271,8 @@ export class Batch {
273
271
  * @param {Effect[]} effects
274
272
  */
275
273
  #defer_effects(effects) {
276
- for (const e of effects) {
277
- if ((e.f & DIRTY) !== 0) {
278
- this.#dirty_effects.add(e);
279
- } else if ((e.f & MAYBE_DIRTY) !== 0) {
280
- this.#maybe_dirty_effects.add(e);
281
- }
282
-
283
- // Since we're not executing these effects now, we need to clear any WAS_MARKED flags
284
- // so that other batches can correctly reach these effects during their own traversal
285
- this.#clear_marked(e.deps);
286
-
287
- // mark as clean so they get scheduled if they depend on pending async state
288
- set_signal_status(e, CLEAN);
289
- }
290
- }
291
-
292
- /**
293
- * @param {Value[] | null} deps
294
- */
295
- #clear_marked(deps) {
296
- if (deps === null) return;
297
-
298
- for (const dep of deps) {
299
- if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
300
- continue;
301
- }
302
-
303
- dep.f ^= WAS_MARKED;
304
-
305
- this.#clear_marked(/** @type {Derived} */ (dep).deps);
274
+ for (var i = 0; i < effects.length; i += 1) {
275
+ defer_effect(effects[i], this.#dirty_effects, this.#maybe_dirty_effects);
306
276
  }
307
277
  }
308
278
 
@@ -313,7 +283,7 @@ export class Batch {
313
283
  * @param {any} value
314
284
  */
315
285
  capture(source, value) {
316
- if (!this.previous.has(source)) {
286
+ if (value !== UNINITIALIZED && !this.previous.has(source)) {
317
287
  this.previous.set(source, value);
318
288
  }
319
289
 
@@ -383,14 +353,6 @@ export class Batch {
383
353
  var previous_batch_values = batch_values;
384
354
  var is_earlier = true;
385
355
 
386
- /** @type {EffectTarget} */
387
- var dummy_target = {
388
- parent: null,
389
- effect: null,
390
- effects: [],
391
- render_effects: []
392
- };
393
-
394
356
  for (const batch of batches) {
395
357
  if (batch === this) {
396
358
  is_earlier = false;
@@ -439,10 +401,10 @@ export class Batch {
439
401
  batch.apply();
440
402
 
441
403
  for (const root of queued_root_effects) {
442
- batch.#traverse_effect_tree(root, dummy_target);
404
+ batch.#traverse_effect_tree(root, [], []);
443
405
  }
444
406
 
445
- // TODO do we need to do anything with `target`? defer block effects?
407
+ // TODO do we need to do anything with the dummy effect arrays?
446
408
 
447
409
  batch.deactivate();
448
410
  }
@@ -959,13 +921,18 @@ export function fork(fn) {
959
921
 
960
922
  flushSync(fn);
961
923
 
962
- batch_values = null;
963
-
964
924
  // revert state changes
965
925
  for (var [source, value] of batch.previous) {
966
926
  source.v = value;
967
927
  }
968
928
 
929
+ // make writable deriveds dirty, so they recalculate correctly
930
+ for (source of batch.current.keys()) {
931
+ if ((source.f & DERIVED) !== 0) {
932
+ set_signal_status(source, DIRTY);
933
+ }
934
+ }
935
+
969
936
  return {
970
937
  commit: async () => {
971
938
  if (committed) {
@@ -981,9 +948,10 @@ export function fork(fn) {
981
948
 
982
949
  batch.is_fork = false;
983
950
 
984
- // apply changes
951
+ // apply changes and update write versions so deriveds see the change
985
952
  for (var [source, value] of batch.current) {
986
953
  source.v = value;
954
+ source.wv = increment_write_version();
987
955
  }
988
956
 
989
957
  // trigger any `$state.eager(...)` expressions with the new state.