Skip to content

feat: add $log rune #9670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 39 commits into from
Closed

feat: add $log rune #9670

wants to merge 39 commits into from

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Nov 27, 2023

$log

The $log rune is roughly equivalent to console.log, with the exception that when anything passed to the rune changes, the latest values will be logged out. $log tracks reactive state deeply, meaning that mutating something from with an object or array using fine-grain reactivity will be tracked.

<script>
	let count = $state(0);

	$log({ count }); // will console.log when count changes
</script>

<button onclick={() => count++}>Increment</button>

$log only works during development.

$log.break

This works just like $log, except runtime execution will be paused via a debugger statement.

<script>
	let count = $state(0);

	$log.break({ count });
</script>

<button onclick={() => count++}>Increment</button>

$log.table

This works just like $log, but triggers console.table instead of console.log.

<script>
	let count = $state(0);

	$log.table({ count });
</script>

<button onclick={() => count++}>Increment</button>

$log.trace

This works just like $log, but in addition to logging the values, it will also trace any mutations to fine-grain reactive state and log the sourcecode location to help find the cause for something changing.

<script>
	let count = $state(0);

	$log.trace({ count });
</script>

<button onclick={() => count++}>Increment</button>

Copy link

changeset-bot bot commented Nov 27, 2023

🦋 Changeset detected

Latest commit: d89d54d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

vercel bot commented Nov 27, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
svelte-5-preview ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 29, 2023 0:25am

/**
* @template V
* @param {import('./types.js').Signal<V>} signal
* @param {V} value
* @returns {V}
*/
export function set(signal, value) {
attach_trace(signal);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should attach_trace be called inside set_signal_value, rather than immediately before it? It might save us from forgetting to attach the trace in some future function that calls set_signal_value.

I see there are two occurrences of set_signal_value that don't have a preceding attach_trace, inside update_each_item_block — I don't fully understand the code, but is there any real downside to always attaching the trace when a signal is set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for it being like this is to ensure that the same depth in the stack for the place the user does the mutation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't we just need to just that number by 1 if attach_trace was inside set_signal_value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attach_trace is used in various places where set_signal_value can be the 2nd or 4th in the call-stack from memory so I tried to ensure it was always the same place. Maybe there's a more sane way of doing this.

@Rich-Harris
Copy link
Member

I'm trying this locally, and unfortunately the $log.trace sourcemaps are way off — it's getting the right file, but completely the wrong line:


image

(It should be line 36, where the count += 1 assignment is. Possibly to do with the event handler hoisting?)

I think we need to fix that before we can merge this, as it feels quite broken at the moment. (Side-note, getting a local copy of svelte to work in the demo SvelteKit app was surprisingly hard — in addition to having to add svelte to optimizeDeps.exclude I basically had to manage my own symlinks, because pnpm link didn't work. I have no idea why.)

At least $log.trace takes me to a sourcemapped file though. $log.break takes me to generated code, which is a little disorienting:


image

It took me a while to figure out how to inspect the values I care about. My first instinct was to look inside Counter for count, but that's obviously the signal rather than the value itself:


image

If we can't get sourcemaps to work with debugger, then it might be helpful if we can at least generate something like this, so that the values I care about are immediately visible:

$.log_break(() => [{ count: $.get(count) }], (count) => {
  debugger;
});

Another thing I wonder is whether we could deliver a more useful call stack than this:


image

Would it be a crazy thing to do to add the (count) => { debugger } function to all the signals we encounter during deep_read, so that we can call it on set rather than inside the effect? That way, the call stack would include the event handler (or whatever) that initiated the state change — user code rather than a scenic tour of runtime.js.

@Rich-Harris
Copy link
Member

At least $log.trace takes me to a sourcemapped file though. $log.break takes me to generated code, which is a little disorienting

it appears that debugger ignoring sourcemaps is a Firefox issue — in Chrome the call stack includes sourcemapped code (the sourcemaps are still off though)

@trueadm
Copy link
Contributor Author

trueadm commented Nov 28, 2023

Would it be a crazy thing to do to add the (count) => { debugger } function to all the signals we encounter during deep_read, so that we can call it on set rather than inside the effect? That way, the call stack would include the event handler (or whatever) that initiated the state change — user code rather than a scenic tour of runtime.js.

So we'd have an arrow function per each signal as a callback? Not sure what you mean here.

I think we need to fix that before we can merge this, as it feels quite broken at the moment. (Side-note, getting a local copy of svelte to work in the demo SvelteKit app was surprisingly hard — in addition to having to add svelte to optimizeDeps.exclude I basically had to manage my own symlinks, because pnpm link didn't work. I have no idea why.)

Seems like something is going wrong with the hoisting transform for event handlers. I'll take a look today.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 28, 2023

Digging more into the sourcemap issues:

  • All the sourcemaps seem to be off by 1 line with JS?
  • Event delegation doesn't seem to affect the line numbers for me
  • Enabling TS seems to throw the line numbers off even more for sourcemaps. Digging into the AST and the line positions are just missing it seems for a bunch of AST nodes?!

@Rich-Harris
Copy link
Member

So we'd have an arrow function per each signal as a callback? Not sure what you mean here.

During deep_read, instead of just doing get.call(value) we do something like this:

function deep_read(value, fn) {
  // ...
  signals = expose(() => get.call(value));
  for (const signal of signals) {
    signal.d = fn;
  }
  // ...
}

(Note that this requires expose to return an array of signals, to handle cases like this):

a = $state(1);
b = $state(1);

return {
  get total() {
    return a + b;
  }
};

Then log_break would look something like this...

export function log_break(get_values, break_fn) {
  let initial = true;

  pre_effect(() => {
    const values = get_values();
    deep_read(values, break_fn);
  });
}

...and signal.d would be invoked inside set_signal_value.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 28, 2023

@Rich-Harris That approach you outlined was my kind of the original approach to dealing with this.

However it broke down in a couple of cases:

  • we also use expose between prop bindings, so they interfered with each other. We can work around it but it wasn't trivial.
  • it means that we would over-fire the trace function, when we mostly really only care about the last mutation. I guess this isn't a big issue though.

However, if you want to take a bash in making it work, then go for it :)

@benmccann
Copy link
Member

Someone of Discord noticed an issue with the docs: https://discord.com/channels/457912077277855764/1153350350158450758/1178846700639629373

It's a bit weird to have the old runes, "How to opt in" in the middle, and then the new $log runes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants