svelte 5.43.3 → 5.43.5

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.43.3",
5
+ "version": "5.43.5",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -159,6 +159,16 @@ export function BindDirective(node, context) {
159
159
 
160
160
  mark_subtree_dynamic(context.path);
161
161
 
162
+ const [get, set] = node.expression.expressions;
163
+ // We gotta jump across the getter/setter functions to avoid the expression metadata field being reset to null
164
+ context.visit(get.type === 'ArrowFunctionExpression' ? get.body : get, {
165
+ ...context.state,
166
+ expression: node.metadata.expression
167
+ });
168
+ context.visit(set.type === 'ArrowFunctionExpression' ? set.body : set, {
169
+ ...context.state,
170
+ expression: node.metadata.expression
171
+ });
162
172
  return;
163
173
  }
164
174
 
@@ -247,7 +257,8 @@ export function BindDirective(node, context) {
247
257
 
248
258
  node.metadata = {
249
259
  binding_group_name: group_name,
250
- parent_each_blocks: each_blocks
260
+ parent_each_blocks: each_blocks,
261
+ expression: node.metadata.expression
251
262
  };
252
263
  }
253
264
 
@@ -255,5 +266,5 @@ export function BindDirective(node, context) {
255
266
  w.bind_invalid_each_rest(binding.node, binding.node.name);
256
267
  }
257
268
 
258
- context.next();
269
+ context.next({ ...context.state, expression: node.metadata.expression });
259
270
  }
@@ -23,6 +23,9 @@ export function StyleDirective(node, context) {
23
23
  if (binding.kind !== 'normal') {
24
24
  node.metadata.expression.has_state = true;
25
25
  }
26
+ if (binding.blocker) {
27
+ node.metadata.expression.dependencies.add(binding);
28
+ }
26
29
  }
27
30
  } else {
28
31
  context.next();
@@ -30,9 +33,7 @@ export function StyleDirective(node, context) {
30
33
  for (const chunk of get_attribute_chunks(node.value)) {
31
34
  if (chunk.type !== 'ExpressionTag') continue;
32
35
 
33
- node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
34
- node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
35
- node.metadata.expression.has_await ||= chunk.metadata.expression.has_await;
36
+ node.metadata.expression.merge(chunk.metadata.expression);
36
37
  }
37
38
  }
38
39
  }
@@ -250,10 +250,13 @@ export function BindDirective(node, context) {
250
250
 
251
251
  let statement = defer ? b.stmt(b.call('$.effect', b.thunk(call))) : b.stmt(call);
252
252
 
253
- // TODO this doesn't account for function bindings
254
- if (node.metadata.binding?.blocker) {
253
+ if (node.metadata.expression.is_async()) {
255
254
  statement = b.stmt(
256
- b.call(b.member(node.metadata.binding.blocker, b.id('then')), b.thunk(b.block([statement])))
255
+ b.call(
256
+ '$.run_after_blockers',
257
+ node.metadata.expression.blockers(),
258
+ b.thunk(b.block([statement]))
259
+ )
257
260
  );
258
261
  }
259
262
 
@@ -484,21 +484,6 @@ function setup_select_synchronization(value_binding, context) {
484
484
  );
485
485
  }
486
486
 
487
- /**
488
- * @param {ExpressionMetadata} target
489
- * @param {ExpressionMetadata} source
490
- */
491
- function merge_metadata(target, source) {
492
- target.has_assignment ||= source.has_assignment;
493
- target.has_await ||= source.has_await;
494
- target.has_call ||= source.has_call;
495
- target.has_member_expression ||= source.has_member_expression;
496
- target.has_state ||= source.has_state;
497
-
498
- for (const r of source.references) target.references.add(r);
499
- for (const b of source.dependencies) target.dependencies.add(b);
500
- }
501
-
502
487
  /**
503
488
  * @param {AST.ClassDirective[]} class_directives
504
489
  * @param {ComponentContext} context
@@ -514,7 +499,7 @@ export function build_class_directives_object(
514
499
  const metadata = new ExpressionMetadata();
515
500
 
516
501
  for (const d of class_directives) {
517
- merge_metadata(metadata, d.metadata.expression);
502
+ metadata.merge(d.metadata.expression);
518
503
 
519
504
  const expression = /** @type Expression */ (context.visit(d.expression));
520
505
  properties.push(b.init(d.name, expression));
@@ -541,7 +526,7 @@ export function build_style_directives_object(
541
526
  const metadata = new ExpressionMetadata();
542
527
 
543
528
  for (const d of style_directives) {
544
- merge_metadata(metadata, d.metadata.expression);
529
+ metadata.merge(d.metadata.expression);
545
530
 
546
531
  const expression =
547
532
  d.value === true
@@ -171,8 +171,6 @@ export function build_component(node, component_name, context) {
171
171
  attribute.value,
172
172
  context,
173
173
  (value, metadata) => {
174
- if (!metadata.has_state && !metadata.has_await) return value;
175
-
176
174
  // When we have a non-simple computation, anything other than an Identifier or Member expression,
177
175
  // then there's a good chance it needs to be memoized to avoid over-firing when read within the
178
176
  // child component (e.g. `active={i === index}`)
@@ -198,7 +196,12 @@ export function build_component(node, component_name, context) {
198
196
  push_prop(b.init(attribute.name, value));
199
197
  }
200
198
  } else if (attribute.type === 'BindDirective') {
201
- const expression = /** @type {Expression} */ (context.visit(attribute.expression));
199
+ const expression = /** @type {Expression} */ (
200
+ context.visit(attribute.expression, { ...context.state, memoizer })
201
+ );
202
+
203
+ // Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers
204
+ memoizer.check_blockers(attribute.metadata.expression);
202
205
 
203
206
  if (
204
207
  dev &&
@@ -122,7 +122,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
122
122
 
123
123
  return {
124
124
  value: memoize(expression, chunk.metadata.expression),
125
- has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.has_await
125
+ has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.is_async()
126
126
  };
127
127
  }
128
128
 
@@ -170,7 +170,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
170
170
  if (class_directives.length) {
171
171
  next = build_class_directives_object(class_directives, context);
172
172
  has_state ||= class_directives.some(
173
- (d) => d.metadata.expression.has_state || d.metadata.expression.has_await
173
+ (d) => d.metadata.expression.has_state || d.metadata.expression.is_async()
174
174
  );
175
175
 
176
176
  if (has_state) {
@@ -240,7 +240,7 @@ export function build_set_style(node_id, attribute, style_directives, context) {
240
240
  if (style_directives.length) {
241
241
  next = build_style_directives_object(style_directives, context);
242
242
  has_state ||= style_directives.some(
243
- (d) => d.metadata.expression.has_state || d.metadata.expression.has_await
243
+ (d) => d.metadata.expression.has_state || d.metadata.expression.is_async()
244
244
  );
245
245
 
246
246
  if (has_state) {
@@ -31,11 +31,7 @@ export class Memoizer {
31
31
  * @param {boolean} memoize_if_state
32
32
  */
33
33
  add(expression, metadata, memoize_if_state = false) {
34
- for (const binding of metadata.dependencies) {
35
- if (binding.blocker) {
36
- this.#blockers.add(binding.blocker);
37
- }
38
- }
34
+ this.check_blockers(metadata);
39
35
 
40
36
  const should_memoize =
41
37
  metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state);
@@ -52,6 +48,17 @@ export class Memoizer {
52
48
  return id;
53
49
  }
54
50
 
51
+ /**
52
+ * @param {ExpressionMetadata} metadata
53
+ */
54
+ check_blockers(metadata) {
55
+ for (const binding of metadata.dependencies) {
56
+ if (binding.blocker) {
57
+ this.#blockers.add(binding.blocker);
58
+ }
59
+ }
60
+ }
61
+
55
62
  apply() {
56
63
  return [...this.#sync, ...this.#async].map((memo, i) => {
57
64
  memo.id.name = `$${i}`;
@@ -348,6 +355,7 @@ export function validate_binding(state, binding, expression) {
348
355
  b.call(
349
356
  '$.validate_binding',
350
357
  b.literal(state.analysis.source.slice(binding.start, binding.end)),
358
+ binding.metadata.expression.blockers(),
351
359
  b.thunk(
352
360
  state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj
353
361
  ),
@@ -107,6 +107,9 @@ export function build_inline_component(node, expression, context) {
107
107
 
108
108
  push_prop(b.prop('init', b.key(attribute.name), value));
109
109
  } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
110
+ // Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers
111
+ optimiser.check_blockers(attribute.metadata.expression);
112
+
110
113
  if (attribute.expression.type === 'SequenceExpression') {
111
114
  const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression))
112
115
  .expressions;
@@ -121,6 +121,8 @@ export function build_element_attributes(node, context, transform) {
121
121
  expression = b.call(expression.expressions[0]);
122
122
  }
123
123
 
124
+ expression = transform(expression, attribute.metadata.expression);
125
+
124
126
  if (is_content_editable_binding(attribute.name)) {
125
127
  content = expression;
126
128
  } else if (attribute.name === 'value' && node.name === 'textarea') {
@@ -347,16 +347,11 @@ export class PromiseOptimiser {
347
347
  #blockers = new Set();
348
348
 
349
349
  /**
350
- *
351
350
  * @param {Expression} expression
352
351
  * @param {ExpressionMetadata} metadata
353
352
  */
354
353
  transform = (expression, metadata) => {
355
- for (const binding of metadata.dependencies) {
356
- if (binding.blocker) {
357
- this.#blockers.add(binding.blocker);
358
- }
359
- }
354
+ this.check_blockers(metadata);
360
355
 
361
356
  if (metadata.has_await) {
362
357
  this.has_await = true;
@@ -368,6 +363,17 @@ export class PromiseOptimiser {
368
363
  return expression;
369
364
  };
370
365
 
366
+ /**
367
+ * @param {ExpressionMetadata} metadata
368
+ */
369
+ check_blockers(metadata) {
370
+ for (const binding of metadata.dependencies) {
371
+ if (binding.blocker) {
372
+ this.#blockers.add(binding.blocker);
373
+ }
374
+ }
375
+ }
376
+
371
377
  apply() {
372
378
  if (this.expressions.length === 0) {
373
379
  return b.empty;
@@ -115,6 +115,21 @@ export class ExpressionMetadata {
115
115
  is_async() {
116
116
  return this.has_await || this.#get_blockers().size > 0;
117
117
  }
118
+
119
+ /**
120
+ * @param {ExpressionMetadata} source
121
+ */
122
+ merge(source) {
123
+ this.has_state ||= source.has_state;
124
+ this.has_call ||= source.has_call;
125
+ this.has_await ||= source.has_await;
126
+ this.has_member_expression ||= source.has_member_expression;
127
+ this.has_assignment ||= source.has_assignment;
128
+ this.#blockers = null; // so that blockers are recalculated
129
+
130
+ for (const r of source.references) this.references.add(r);
131
+ for (const b of source.dependencies) this.dependencies.add(b);
132
+ }
118
133
  }
119
134
 
120
135
  /**
@@ -6,6 +6,13 @@ export const BLOCK_EFFECT = 1 << 4;
6
6
  export const BRANCH_EFFECT = 1 << 5;
7
7
  export const ROOT_EFFECT = 1 << 6;
8
8
  export const BOUNDARY_EFFECT = 1 << 7;
9
+ /**
10
+ * Indicates that a reaction is connected to an effect root — either it is an effect,
11
+ * or it is a derived that is depended on by at least one effect. If a derived has
12
+ * no dependents, we can disconnect it from the graph, allowing it to either be
13
+ * GC'd or reconnected later if an effect comes to depend on it again
14
+ */
15
+ export const CONNECTED = 1 << 9;
9
16
  export const CLEAN = 1 << 10;
10
17
  export const DIRTY = 1 << 11;
11
18
  export const MAYBE_DIRTY = 1 << 12;
@@ -26,8 +33,6 @@ export const EFFECT_PRESERVED = 1 << 19;
26
33
  export const USER_EFFECT = 1 << 20;
27
34
 
28
35
  // Flags exclusive to deriveds
29
- export const UNOWNED = 1 << 8;
30
- export const DISCONNECTED = 1 << 9;
31
36
  /**
32
37
  * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
33
38
  * Will be lifted during execution of the derived and during checking its dirty state (both are necessary
@@ -20,7 +20,7 @@ import {
20
20
  TEMPLATE_USE_MATHML,
21
21
  TEMPLATE_USE_SVG
22
22
  } from '../../../constants.js';
23
- import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, TEXT_NODE } from '#client/constants';
23
+ import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, EFFECT_RAN, TEXT_NODE } from '#client/constants';
24
24
 
25
25
  /**
26
26
  * @param {TemplateNode} start
@@ -344,7 +344,13 @@ export function comment() {
344
344
  */
345
345
  export function append(anchor, dom) {
346
346
  if (hydrating) {
347
- /** @type {Effect} */ (active_effect).nodes_end = hydrate_node;
347
+ var effect = /** @type {Effect} */ (active_effect);
348
+ // When hydrating and outer component and an inner component is async, i.e. blocked on a promise,
349
+ // then by the time the inner resolves we have already advanced to the end of the hydrated nodes
350
+ // of the parent component. Check for defined for that reason to avoid rewinding the parent's end marker.
351
+ if ((effect.f & EFFECT_RAN) === 0 || effect.nodes_end === null) {
352
+ effect.nodes_end = hydrate_node;
353
+ }
348
354
  hydrate_next();
349
355
  return;
350
356
  }
@@ -102,7 +102,8 @@ export {
102
102
  for_await_track_reactivity_loss,
103
103
  run,
104
104
  save,
105
- track_reactivity_loss
105
+ track_reactivity_loss,
106
+ run_after_blockers
106
107
  } from './reactivity/async.js';
107
108
  export { eager, flushSync as flush } from './reactivity/batch.js';
108
109
  export {
@@ -84,6 +84,14 @@ export function flatten(blockers, sync, async, fn) {
84
84
  }
85
85
  }
86
86
 
87
+ /**
88
+ * @param {Array<Promise<void>>} blockers
89
+ * @param {(values: Value[]) => any} fn
90
+ */
91
+ export function run_after_blockers(blockers, fn) {
92
+ flatten(blockers, [], [], fn);
93
+ }
94
+
87
95
  /**
88
96
  * Captures the current effect context so that we can restore it after
89
97
  * some asynchronous work has happened (so that e.g. `await a + b`
@@ -9,15 +9,14 @@ import {
9
9
  EFFECT_PRESERVED,
10
10
  MAYBE_DIRTY,
11
11
  STALE_REACTION,
12
- UNOWNED,
13
12
  ASYNC,
14
- WAS_MARKED
13
+ WAS_MARKED,
14
+ CONNECTED
15
15
  } from '#client/constants';
16
16
  import {
17
17
  active_reaction,
18
18
  active_effect,
19
19
  set_signal_status,
20
- skip_reaction,
21
20
  update_reaction,
22
21
  increment_write_version,
23
22
  set_active_effect,
@@ -27,7 +26,7 @@ import {
27
26
  import { equals, safe_equals } from './equality.js';
28
27
  import * as e from '../errors.js';
29
28
  import * as w from '../warnings.js';
30
- import { async_effect, destroy_effect, teardown } from './effects.js';
29
+ import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
31
30
  import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
32
31
  import { get_stack } from '../dev/tracing.js';
33
32
  import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -61,9 +60,7 @@ export function derived(fn) {
61
60
  ? /** @type {Derived} */ (active_reaction)
62
61
  : null;
63
62
 
64
- if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) {
65
- flags |= UNOWNED;
66
- } else {
63
+ if (active_effect !== null) {
67
64
  // Since deriveds are evaluated lazily, any effects created inside them are
68
65
  // created too late to ensure that the parent effect is added to the tree
69
66
  active_effect.f |= EFFECT_PRESERVED;
@@ -368,12 +365,16 @@ export function update_derived(derived) {
368
365
  return;
369
366
  }
370
367
 
368
+ // During time traveling we don't want to reset the status so that
369
+ // traversal of the graph in the other batches still happens
371
370
  if (batch_values !== null) {
372
- batch_values.set(derived, derived.v);
371
+ // only cache the value if we're in a tracking context, otherwise we won't
372
+ // clear the cache in `mark_reactions` when dependencies are updated
373
+ if (effect_tracking()) {
374
+ batch_values.set(derived, derived.v);
375
+ }
373
376
  } else {
374
- var status =
375
- (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
376
-
377
+ var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN;
377
378
  set_signal_status(derived, status);
378
379
  }
379
380
  }
@@ -25,7 +25,6 @@ import {
25
25
  ROOT_EFFECT,
26
26
  EFFECT_TRANSPARENT,
27
27
  DERIVED,
28
- UNOWNED,
29
28
  CLEAN,
30
29
  EAGER_EFFECT,
31
30
  HEAD_EFFECT,
@@ -33,7 +32,8 @@ import {
33
32
  EFFECT_PRESERVED,
34
33
  STALE_REACTION,
35
34
  USER_EFFECT,
36
- ASYNC
35
+ ASYNC,
36
+ CONNECTED
37
37
  } from '#client/constants';
38
38
  import * as e from '../errors.js';
39
39
  import { DEV } from 'esm-env';
@@ -48,11 +48,11 @@ import { without_reactive_context } from '../dom/elements/bindings/shared.js';
48
48
  * @param {'$effect' | '$effect.pre' | '$inspect'} rune
49
49
  */
50
50
  export function validate_effect(rune) {
51
- if (active_effect === null && active_reaction === null) {
52
- e.effect_orphan(rune);
53
- }
51
+ if (active_effect === null) {
52
+ if (active_reaction === null) {
53
+ e.effect_orphan(rune);
54
+ }
54
55
 
55
- if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
56
56
  e.effect_in_unowned_derived();
57
57
  }
58
58
 
@@ -103,7 +103,7 @@ function create_effect(type, fn, sync, push = true) {
103
103
  deps: null,
104
104
  nodes_start: null,
105
105
  nodes_end: null,
106
- f: type | DIRTY,
106
+ f: type | DIRTY | CONNECTED,
107
107
  first: null,
108
108
  fn,
109
109
  last: null,
@@ -23,18 +23,18 @@ import {
23
23
  DIRTY,
24
24
  BRANCH_EFFECT,
25
25
  EAGER_EFFECT,
26
- UNOWNED,
27
26
  MAYBE_DIRTY,
28
27
  BLOCK_EFFECT,
29
28
  ROOT_EFFECT,
30
29
  ASYNC,
31
- WAS_MARKED
30
+ WAS_MARKED,
31
+ CONNECTED
32
32
  } from '#client/constants';
33
33
  import * as e from '../errors.js';
34
34
  import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
35
35
  import { get_stack, tag_proxy } from '../dev/tracing.js';
36
36
  import { component_context, is_runes } from '../context.js';
37
- import { Batch, eager_block_effects, schedule_effect } from './batch.js';
37
+ import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
38
38
  import { proxy } from '../proxy.js';
39
39
  import { execute_derived } from './deriveds.js';
40
40
 
@@ -211,7 +211,8 @@ export function internal_set(source, value) {
211
211
  if ((source.f & DIRTY) !== 0) {
212
212
  execute_derived(/** @type {Derived} */ (source));
213
213
  }
214
- set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
214
+
215
+ set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY);
215
216
  }
216
217
 
217
218
  source.wv = increment_write_version();
@@ -333,9 +334,17 @@ function mark_reactions(signal, status) {
333
334
  }
334
335
 
335
336
  if ((flags & DERIVED) !== 0) {
337
+ var derived = /** @type {Derived} */ (reaction);
338
+
339
+ batch_values?.delete(derived);
340
+
336
341
  if ((flags & WAS_MARKED) === 0) {
337
- reaction.f |= WAS_MARKED;
338
- mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
342
+ // Only connected deriveds can be reliably unmarked right away
343
+ if (flags & CONNECTED) {
344
+ reaction.f |= WAS_MARKED;
345
+ }
346
+
347
+ mark_reactions(derived, MAYBE_DIRTY);
339
348
  }
340
349
  } else if (not_dirty) {
341
350
  if ((flags & BLOCK_EFFECT) !== 0) {