Skip to content
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

Combine Router and Renderer #4161

Merged
merged 37 commits into from
Mar 2, 2022
Merged

Combine Router and Renderer #4161

merged 37 commits into from
Mar 2, 2022

Conversation

Rich-Harris
Copy link
Member

This has been on my wishlist for a while — taking the Router and Renderer classes, which are somewhat tightly coupled and have a confusing relationship to each other, and converting them to a single client entity. It does mean a big ass client.js file (though we could probably extract out some utilities), but overall I think it's much easier to understand the flow of things when it's all colocated.

I opted for a revealing module pattern in this PR rather than a new class for a couple of reasons:

  • It minifies better — the client is more than 2kb lighter before gzip (less of a difference after gzip but every little helps)
  • We can have private methods and state without using syntax that Safari doesn't like
  • No this nonsense — $app/navigation can simply re-export the public methods without needing to wrap them

There's still some tidying up to do, but on the whole this feels like a good direction to me — less source code, less built code, less indirection.

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpx changeset and following the prompts. All changesets should be patch until SvelteKit 1.0

@changeset-bot
Copy link

changeset-bot bot commented Mar 1, 2022

⚠️ No Changeset found

Latest commit: 9c00c58

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@Rich-Harris Rich-Harris marked this pull request as draft March 1, 2022 15:53
Copy link
Member

@benmccann benmccann left a comment

Choose a reason for hiding this comment

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

This still feels like throwing the baby out with the bathwater to me. I totally agree the current classes are terribly structured, but I think there are better solutions than putting everything into a single file. At the end of the day, I might just have to agree to disagree, but I'd love if we could get other opinions on this approach vs #4101

Comment on lines +30 to +35
let scroll_positions = {};
try {
scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
} catch {
// do nothing
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this could be a const as it doesn't appear to be updated after initialization. E.g. you could do something like:

const scroll_positions = (() => {
try {
	return JSON.parse(sessionStorage[SCROLL_KEY]);
} catch {}
return {};
})();

Copy link
Member Author

Choose a reason for hiding this comment

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

I've always felt iffy about IIFEs

Copy link
Member

Choose a reason for hiding this comment

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

Another option to make it a const might be:

const scroll_positions = {};
try {
	Object.assign(scroll_positions, JSON.parse(sessionStorage[SCROLL_KEY]));
} catch {
	// do nothing
}

packages/kit/src/runtime/client/client.js Outdated Show resolved Hide resolved
}

/** @param {URL} url */
function _parse(url) {
Copy link
Member

@benmccann benmccann Mar 1, 2022

Choose a reason for hiding this comment

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

I think _parse doesn't quite represent what this method does or is a bit vaguely named. How about _get_navigation_candidates?

Copy link
Member Author

Choose a reason for hiding this comment

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

changed to get_navigation_intent

/** @type {Record<string, any>} */
let stuff = {};

/** @type {import('./types').NavigationResult | undefined} */
Copy link
Member

Choose a reason for hiding this comment

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

I think we should rename NavigationResult to LoadResult

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree that the terminology could be tightened up a bit, though I think there's value in thinking of a 'load' as something that relates to a single node (i.e. layout/page component) in the branch, since that's where you might find a load function

packages/kit/src/runtime/client/client.js Outdated Show resolved Hide resolved
Rich-Harris and others added 2 commits March 2, 2022 13:34
Co-authored-by: Maurício Kishi <mrkishi@users.noreply.github.com>

/** @type {import('./types').NavigationState} */
let current = {
// @ts-ignore - we need the initial value to be null
Copy link
Member

Choose a reason for hiding this comment

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

is this true? could we not just do new URL(window.location.href)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Conceptually, upon hydration we're navigating from null to location.href, and this is reflected in places like

if (!current.url || url.href !== current.url.href) {
and
const changed = current.url && {
url: id !== current.url.pathname + current.url.search,
params: Object.keys(params).filter((key) => current.params[key] !== params[key]),
session: session_id !== current.session_id
};

};

/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
const loading = {
Copy link
Member

Choose a reason for hiding this comment

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

a better name for this might be load_cache

a related note - I just saw someone on Discord raise the issue that if they move their mouse between two links they will be prefetched over and over again. possibly we could make this more than a single-membered cache in the future

Copy link
Member Author

Choose a reason for hiding this comment

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

renamed. re multiple prefetches, the current behaviour is by design, but if they want to open an issue then we can discuss further

if (!ready) return;
session_id += 1;

const intent = get_navigation_intent(new URL(location.href));
Copy link
Member

Choose a reason for hiding this comment

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

should we just call invalidate here?

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't have quite the right semantics — if session changes we want to re-run load functions that depend on session, not those that fetch(location.href)

const current_token = (token = {});
let navigation_result = await get_navigation_result(intent, no_cache);

if (!navigation_result && intent.url.pathname === location.pathname) {
Copy link
Member

Choose a reason for hiding this comment

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

something that might be worth a comment - I'm not understanding the second half of this check. if you want to navigate to the page we're already on, isn't that essentially just a no-op? why do we want to show a 404 page in that case?

Copy link
Member Author

Choose a reason for hiding this comment

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

added a comment

}

// abort if user navigated during update
if (token !== current_token) return;
Copy link
Member

Choose a reason for hiding this comment

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

I noticed when working on #4101 that this token stuff looks broken in master. I didn't attempt to fix it and it looks like it's not addressed here either. Assuming I haven't missed something, current_token is only ever set to {} and so this is a no-op check

Copy link
Member

Choose a reason for hiding this comment

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

update() can be called by a different navigation/redirect between the time it started and the time this check is made, meaning token will point to a different {} than this invocation's current_token, and in that case we abort (because this call to update() was superseded).

Copy link
Member Author

Choose a reason for hiding this comment

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

it's working as intended as far as I can see. consider this simplified version:

let token;

async function log(message) {
  const current_token = (token = {});
  await Promise.resolve();
  if (token !== current_token) return;

  console.log(message);
}

// only b is logged; a is aborted
log('a');
log('b');

Copy link
Member

Choose a reason for hiding this comment

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

Oh. I see. That's a bit of a subtle thing that relies on === checking that they're the same instance. I was just looking at it and seeing that they were both always {} and doing more of a checking that the values were equivalent in my head. Maybe we could either add a comment or rename to current_token_instance and token_instance?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's clear enough, given the comment immediately above the check

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.

3 participants