experiment-s2
Dominic Gannaway 17 hours ago
parent 0c0fd47b39
commit 91399eeb85

@ -69,6 +69,7 @@ import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js'; import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
@ -158,6 +159,7 @@ const visitors = {
LetDirective, LetDirective,
MemberExpression, MemberExpression,
NewExpression, NewExpression,
AwaitExpression,
OnDirective, OnDirective,
RegularElement, RegularElement,
RenderTag, RenderTag,

@ -0,0 +1,35 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { Context } from '../types' */
import { extract_identifiers } from '../../../utils/ast.js';
import * as w from '../../../warnings.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
const declarator = context.path.at(-1);
const declaration = context.path.at(-2);
const program = context.path.at(-3);
if (context.state.ast_type === 'instance') {
if (
declarator?.type !== 'VariableDeclarator' ||
context.state.function_depth !== 1 ||
declaration?.type !== 'VariableDeclaration' ||
program?.type !== 'Program'
) {
throw new Error('TODO: invalid usage of AwaitExpression in component');
}
for (const declarator of declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = context.state.scope.get(id.name);
if (binding !== null) {
binding.kind = 'derived';
}
}
}
}
context.next();
}

@ -55,6 +55,7 @@ import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TitleElement } from './visitors/TitleElement.js'; import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js'; import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UpdateExpression } from './visitors/UpdateExpression.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { UseDirective } from './visitors/UseDirective.js'; import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js';
@ -131,7 +132,8 @@ const visitors = {
TransitionDirective, TransitionDirective,
UpdateExpression, UpdateExpression,
UseDirective, UseDirective,
VariableDeclaration VariableDeclaration,
AwaitExpression
}; };
/** /**

@ -1,10 +1,11 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement, VariableDeclaration } from 'estree' */
/** @import { AST, Binding } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js'; import * as b from '../../../utils/builders.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import { get_rune } from '../../scope.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
@ -312,3 +313,86 @@ export function create_derived_block_argument(node, context) {
export function create_derived(state, arg) { export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
} }
/**
* @param {(import("estree").ModuleDeclaration | Statement | import("estree").Directive)[]} statements
* @returns {(import("estree").ModuleDeclaration | Statement | import("estree").Directive)[]}
* @param {ComponentContext} context
*/
export function wrap_unsafe_async_statements(statements, context) {
/** @type {(import("estree").ModuleDeclaration | Statement | import("estree").Directive)[]} */
const new_statements = [];
/** @type {Statement[] | null} */
let async_statements = null;
const apply_async_statements = () => {
new_statements.push(
b.stmt(
b.call(
'$.script_effect',
b.thunk(b.block(/** @type {Statement[]} */ (async_statements)))
)
)
);
async_statements = null;
};
const push_async_statement = (/** @type {Statement} */ statement) => {
if (async_statements === null) {
async_statements = [];
}
async_statements.push(statement);
};
for (const statement of statements) {
const visited = /** @type {Statement} */ (context.visit(statement));
if (statement.type === 'ExpressionStatement') {
const rune = get_rune(statement.expression, context.state.scope);
if (rune !== '$effect' && rune !== '$effect.pre') {
push_async_statement(visited);
continue;
}
} else if (statement.type === 'DebuggerStatement') {
push_async_statement(visited);
continue;
} else if (statement.type === 'VariableDeclaration') {
if (statement.declarations.length > 1) {
throw new Error('TODO');
}
const declarator = statement.declarations[0];
const rune = get_rune(declarator.init, context.state.scope);
if (declarator.init?.type === 'AwaitExpression') {
// TODO: do we need to do anything here?
} else if (rune === null || rune === '$state' || rune === '$state.raw') {
const visited_declarator = /** @type {VariableDeclaration} */ (visited).declarations[0];
new_statements.push(b.let(visited_declarator.id));
if (rune === '$state') {
debugger;
}
if (visited_declarator.init != null) {
push_async_statement(
b.stmt(b.assignment('=', visited_declarator.id, visited_declarator.init))
);
}
continue;
} else if (rune !== '$props' && rune !== '$derived' && rune !== '$derived.by') {
debugger;
}
}
if (async_statements !== null) {
apply_async_statements();
}
new_statements.push(visited);
}
if (async_statements !== null) {
apply_async_statements();
}
return new_statements;
}

@ -0,0 +1,17 @@
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {ComponentContext} context
*/
export function AwaitExpression(node, context) {
// Inside component
if (context.state.analysis.instance) {
return b.call('$.await_derived', b.thunk(/** @type {Expression} */ (context.visit(node.argument))));
}
context.next();
}

@ -1,14 +1,14 @@
/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */ /** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js'; import { build_getter, is_prop_source, wrap_unsafe_async_statements } from '../utils.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { add_state_transformers } from './shared/declarations.js'; import { add_state_transformers } from './shared/declarations.js';
/** /**
* @param {Program} _ * @param {Program} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function Program(_, context) { export function Program(node, context) {
if (!context.state.analysis.runes) { if (!context.state.analysis.runes) {
context.state.transform['$$props'] = { context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' }) read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +137,12 @@ export function Program(_, context) {
add_state_transformers(context); add_state_transformers(context);
if (context.state.analysis.instance) {
return {
...node,
body: wrap_unsafe_async_statements(node.body, context)
}
}
context.next(); context.next();
} }

@ -83,7 +83,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
} else if (has_state && !within_bound_contenteditable) { } else if (has_state && !within_bound_contenteditable) {
state.update.push(update); state.update.push(update);
} else { } else {
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); state.init.push(
b.stmt(
b.call('$.script_effect', b.thunk(b.assignment('=', b.member(id, 'nodeValue'), value)))
)
);
} }
} }

@ -191,5 +191,3 @@ export {
} from './internal/client/runtime.js'; } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';
export { create_suspense } from './internal/client/dom/blocks/boundary.js';

@ -5,23 +5,26 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5; export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6; export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7; export const BOUNDARY_EFFECT = 1 << 7;
export const UNOWNED = 1 << 8; export const TEMPLATE_EFFECT = 1 << 8;
export const DISCONNECTED = 1 << 9; export const AWAIT_EFFECT = 1 << 9;
export const CLEAN = 1 << 10; export const UNOWNED = 1 << 10;
export const DIRTY = 1 << 11; export const DISCONNECTED = 1 << 11;
export const MAYBE_DIRTY = 1 << 12; export const CLEAN = 1 << 12;
export const INERT = 1 << 13; export const DIRTY = 1 << 13;
export const DESTROYED = 1 << 14; export const MAYBE_DIRTY = 1 << 14;
export const EFFECT_RAN = 1 << 15; export const INERT = 1 << 15;
export const DESTROYED = 1 << 16;
export const EFFECT_RAN = 1 << 17;
/** 'Transparent' effects do not create a transition boundary */ /** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16; export const EFFECT_TRANSPARENT = 1 << 18;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 17; export const LEGACY_DERIVED_PROP = 1 << 19;
export const INSPECT_EFFECT = 1 << 18; export const INSPECT_EFFECT = 1 << 20;
export const HEAD_EFFECT = 1 << 19; export const HEAD_EFFECT = 1 << 21;
export const EFFECT_HAS_DERIVED = 1 << 20; export const EFFECT_HAS_DERIVED = 1 << 22;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props'); export const LEGACY_PROPS = Symbol('legacy props');
export const PENDING = Symbol();
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');

@ -1,6 +1,8 @@
/** @import { Effect, TemplateNode, } from '#client' */ /** @import { Effect, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; import { UNINITIALIZED } from '../../../../constants.js';
import { AWAIT_EFFECT, BOUNDARY_EFFECT, EFFECT_TRANSPARENT, PENDING } from '../../constants.js';
import { derived } from '../../reactivity/deriveds.js';
import { import {
block, block,
branch, branch,
@ -8,6 +10,7 @@ import {
pause_effect, pause_effect,
resume_effect resume_effect
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { set, source } from '../../reactivity/sources.js';
import { import {
active_effect, active_effect,
active_reaction, active_reaction,
@ -16,7 +19,10 @@ import {
set_active_effect, set_active_effect,
set_active_reaction, set_active_reaction,
set_component_context, set_component_context,
reset_is_throwing_error reset_is_throwing_error,
get,
set_is_within_await,
untrack
} from '../../runtime.js'; } from '../../runtime.js';
import { import {
hydrate_next, hydrate_next,
@ -27,7 +33,7 @@ import {
set_hydrate_node set_hydrate_node
} from '../hydration.js'; } from '../hydration.js';
import { get_next_sibling } from '../operations.js'; import { get_next_sibling } from '../operations.js';
import { queue_boundary_micro_task } from '../task.js'; import { flush_boundary_micro_tasks, queue_boundary_micro_task } from '../task.js';
const ASYNC_INCREMENT = Symbol(); const ASYNC_INCREMENT = Symbol();
const ASYNC_DECREMENT = Symbol(); const ASYNC_DECREMENT = Symbol();
@ -241,3 +247,52 @@ export function trigger_async_boundary(effect, trigger) {
current = current.parent; current = current.parent;
} }
} }
/**
* @template V
* @param {() => Promise<V>} fn
*/
export function await_derived(fn) {
var current = active_effect;
/** @type {Source<V | typeof PENDING>} */
var value = source(PENDING);
/** @type {Promise<V> | typeof UNINITIALIZED} */
var previous_promise = UNINITIALIZED;
var derived_promise = derived(fn);
block(() => {
var promise = get(derived_promise)
try {
get(value)
} catch (e) {
if (e !== PENDING) {
throw e;
}
}
var should_suspend = previous_promise !== promise;
previous_promise = promise;
if (should_suspend) {
set_is_within_await(true);
untrack(() => {
set(value, PENDING);
});
trigger_async_boundary(current, ASYNC_INCREMENT);
// If we're updating, then we need to flush the boundary microtasks
if (current?.parent?.first !== null) {
flush_boundary_micro_tasks();
}
promise.then((v) => {
set(value, v);
trigger_async_boundary(current, ASYNC_DECREMENT);
});
}
return value.v;
}, AWAIT_EFFECT);
return value;
}

@ -107,7 +107,8 @@ export {
template_effect, template_effect,
effect, effect,
user_effect, user_effect,
user_pre_effect user_pre_effect,
script_effect
} from './reactivity/effects.js'; } from './reactivity/effects.js';
export { mutable_state, mutate, set, state } from './reactivity/sources.js'; export { mutable_state, mutate, set, state } from './reactivity/sources.js';
export { export {
@ -129,7 +130,7 @@ export {
update_store, update_store,
mark_store_binding mark_store_binding
} from './reactivity/store.js'; } from './reactivity/store.js';
export { boundary } from './dom/blocks/boundary.js'; export { boundary, await_derived } from './dom/blocks/boundary.js';
export { set_text } from './render.js'; export { set_text } from './render.js';
export { export {
get, get,

@ -16,7 +16,8 @@ import {
set_is_flushing_effect, set_is_flushing_effect,
set_signal_status, set_signal_status,
untrack, untrack,
skip_reaction skip_reaction,
is_within_await
} from '../runtime.js'; } from '../runtime.js';
import { import {
DIRTY, DIRTY,
@ -36,7 +37,8 @@ import {
HEAD_EFFECT, HEAD_EFFECT,
MAYBE_DIRTY, MAYBE_DIRTY,
EFFECT_HAS_DERIVED, EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT BOUNDARY_EFFECT,
TEMPLATE_EFFECT
} from '../constants.js'; } from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
@ -118,27 +120,35 @@ function create_effect(type, fn, sync, push = true) {
effect.component_function = dev_current_component_function; effect.component_function = dev_current_component_function;
} }
if (sync) { var should_run_effect =
var previously_flushing_effect = is_flushing_effect; !is_within_await ||
((type & BRANCH_EFFECT) !== 0 || ((type & BLOCK_EFFECT) !== 0 && (type & TEMPLATE_EFFECT) === 0));
try { if (should_run_effect) {
set_is_flushing_effect(true); if (sync) {
update_effect(effect); var previously_flushing_effect = is_flushing_effect;
effect.f |= EFFECT_RAN;
} catch (e) { try {
destroy_effect(effect); set_is_flushing_effect(true);
throw e; update_effect(effect);
} finally { effect.f |= EFFECT_RAN;
set_is_flushing_effect(previously_flushing_effect); } catch (e) {
destroy_effect(effect);
throw e;
} finally {
set_is_flushing_effect(previously_flushing_effect);
}
} else if (fn !== null) {
schedule_effect(effect);
} }
} else if (fn !== null) {
schedule_effect(effect);
} }
// if an effect has no dependencies, no DOM and no teardown function, // if an effect has no dependencies, no DOM and no teardown function,
// don't bother adding it to the effect tree // don't bother adding it to the effect tree
var inert = var inert =
should_run_effect &&
sync && sync &&
!is_within_await &&
effect.deps === null && effect.deps === null &&
effect.first === null && effect.first === null &&
effect.nodes_start === null && effect.nodes_start === null &&
@ -352,7 +362,7 @@ export function template_effect(fn) {
value: '{expression}' value: '{expression}'
}); });
} }
return block(fn); return block(fn, TEMPLATE_EFFECT);
} }
/** /**
@ -371,6 +381,16 @@ export function branch(fn, push = true) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push); return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push);
} }
/**
* @param {(() => void)} fn
*/
export function script_effect(fn) {
if (active_effect === null || !is_within_await) {
fn();
}
return create_effect(RENDER_EFFECT, () => untrack(fn), true);
}
/** /**
* @param {Effect} effect * @param {Effect} effect
*/ */

@ -25,7 +25,9 @@ import {
ROOT_EFFECT, ROOT_EFFECT,
LEGACY_DERIVED_PROP, LEGACY_DERIVED_PROP,
DISCONNECTED, DISCONNECTED,
BOUNDARY_EFFECT BOUNDARY_EFFECT,
AWAIT_EFFECT,
PENDING
} from './constants.js'; } from './constants.js';
import { import {
flush_idle_tasks, flush_idle_tasks,
@ -58,6 +60,7 @@ let last_scheduled_effect = null;
export let is_flushing_effect = false; export let is_flushing_effect = false;
export let is_destroying_effect = false; export let is_destroying_effect = false;
export let is_within_await = false;
/** @param {boolean} value */ /** @param {boolean} value */
export function set_is_flushing_effect(value) { export function set_is_flushing_effect(value) {
@ -69,6 +72,11 @@ export function set_is_destroying_effect(value) {
is_destroying_effect = value; is_destroying_effect = value;
} }
/** @param {boolean} value */
export function set_is_within_await(value) {
is_within_await = value;
}
// Handle effect queues // Handle effect queues
/** @type {Effect[]} */ /** @type {Effect[]} */
@ -572,6 +580,7 @@ export function update_effect(effect) {
var previous_effect = active_effect; var previous_effect = active_effect;
var previous_component_context = component_context; var previous_component_context = component_context;
var previous_is_within_await = is_within_await;
active_effect = effect; active_effect = effect;
@ -614,9 +623,16 @@ export function update_effect(effect) {
dev_effect_stack.push(effect); dev_effect_stack.push(effect);
} }
} catch (error) { } catch (error) {
if (error === PENDING) {
set_signal_status(effect, DIRTY);
return;
}
handle_error(error, effect, previous_effect, previous_component_context || effect.ctx); handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally { } finally {
active_effect = previous_effect; active_effect = previous_effect;
if ((flags & AWAIT_EFFECT) === 0) {
is_within_await = previous_is_within_await;
}
if (DEV) { if (DEV) {
dev_current_component_function = previous_component_fn; dev_current_component_function = previous_component_fn;
@ -810,7 +826,7 @@ function process_effects(effect, collected_effects) {
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
var sibling = current_effect.next; var sibling = current_effect.next;
if (!is_skippable_branch && (flags & INERT) === 0) { if (!is_skippable_branch && ((flags & (INERT)) === 0 || (flags & (AWAIT_EFFECT)) !== 0)) {
if ((flags & RENDER_EFFECT) !== 0) { if ((flags & RENDER_EFFECT) !== 0) {
if (is_branch) { if (is_branch) {
current_effect.f ^= CLEAN; current_effect.f ^= CLEAN;
@ -820,7 +836,11 @@ function process_effects(effect, collected_effects) {
update_effect(current_effect); update_effect(current_effect);
} }
} catch (error) { } catch (error) {
handle_error(error, current_effect, null, current_effect.ctx); if (error === PENDING) {
set_signal_status(current_effect, DIRTY);
} else {
handle_error(error, current_effect, null, current_effect.ctx);
}
} }
} }
@ -1013,7 +1033,13 @@ export function get(signal) {
} }
} }
return signal.v; value = signal.v;
if (is_within_await && value === PENDING) {
throw PENDING;
}
return value;
} }
/** /**

Loading…
Cancel
Save