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

Hook for initialization? #1538

Closed
hgl opened this issue May 24, 2021 · 26 comments · Fixed by #4742
Closed

Hook for initialization? #1538

hgl opened this issue May 24, 2021 · 26 comments · Fixed by #4742
Labels
feature / enhancement New feature or request
Milestone

Comments

@hgl
Copy link
Contributor

hgl commented May 24, 2021

Is your feature request related to a problem? Please describe.
It should be pretty common for a svelt kit app to utilize external services that you need to initialize clients to talk to them. The doc demonstrates such use case:

import db from '$lib/database';

export async function get({ params }) {
	const { slug } = params;
	const article = await db.get(slug);
	// ...
}

One issue with this code is that it assumes db can be initialized in the module itself either synchronously (which is not always the case) or via top-level await (which svelte kit doesn't seem to support).

Describe the solution you'd like
Offer a hook like async func init() {} that only runs once when it starts?

Describe alternatives you've considered

How important is this feature to you?
Very, since it's pretty awkward to workaround when the initialization is async (e.g., force initialization to be synchronous and only call the async part when actually talk to external services).

Additional context

@ignatiusmb
Copy link
Member

Possibly related to #1530

@hgl
Copy link
Contributor Author

hgl commented May 24, 2021

That issue is about client initialization. But yeah, the two issues should probably be considered en masse to ensure an intuitive API.

@hgl
Copy link
Contributor Author

hgl commented May 24, 2021

Another way to solve this problem is that svelte kit offers a central place to initialize such clients, and pass the clients as an argument to endpoints and hooks:

export async function get({ params, svcs }) {
	const { slug } = params;
	const article = await svcs.db.get(slug);
	// ...
}

This seems to be more conventional than the approach that a module is directly usable as an external service.

@benmccann benmccann added this to the 1.0 milestone May 24, 2021
@benmccann benmccann added the feature / enhancement New feature or request label May 24, 2021
@babichjacob
Copy link
Member

You seem to have missed the request.locals convention in the handle hook. Here's an example from the SvelteKit demo app:

import cookie from 'cookie';
import { v4 as uuid } from '@lukeed/uuid';
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ request, render }) => {
const cookies = cookie.parse(request.headers.cookie || '');
request.locals.userid = cookies.userid || uuid();

There's no rule that you have to re-do work every request (or that the information involved has to be personalized to the user that made the request). Let me demonstrate:

// src/hooks.js
import { stuff } from "db-library";

async function setupDatabaseOrAnythingReally() {
    const dbClient = await stuff({ ...options });
    return dbClient;
}

// Initialize the database *once*
const dbPromise = setupDatabaseOrAnythingReally();

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, render }) {
    // Wait for the db to initialize the first time
    // or instantly get the result of when it was initialized
    const db = await dbPromise;
    
    // Make the database available to the endpoint answering the request
    request.locals = { db };
    return await render(request);
}

locals in an endpoint work the same as your svcs:

// src/routes/blog/[article].json.js
 export async function get({ params, locals }) {
	const { slug } = params;
	const article = await locals.db.get(slug);
	// ...
}

@babichjacob
Copy link
Member

Another maintainer can re-open this if I'm mistaken about what this feature request was about.

@hgl
Copy link
Contributor Author

hgl commented May 26, 2021

Thanks for the tip.

However I think this means that clients are actually initialized at the first connection, potentially delaying it (and other connections) if the initialization is not trivial.

@babichjacob
Copy link
Member

However I think this means that clients are actually initialized at the first connection, potentially delaying it (and other connections) if the initialization is not trivial.

I believe that promises automatically "start executing" the next tick after being created, so this shouldn't be an issue. You can verify by putting a console log at the beginning of the initialization function and see when it shows up.

@hgl
Copy link
Contributor Author

hgl commented May 26, 2021

Oh you are correct. I forgot how promise works lol. Thanks. This works.

@hgl
Copy link
Contributor Author

hgl commented May 27, 2021

Two minor issues I encountered while working with real code:

  1. During development, hook.ts is not executed until there is a connection. I guess this is due to the lazy-loading nature of vite. It's not a big deal in development, but the disparity between dev and prod can potentially be a problem.
  2. During build hook.ts is executed. This can be a problem if your build environment isn't qualified to initialize external services.

For #2, I guess it's executed because svelte kit tries to account for pre-rendered routes? I don't have any such routes, but even if I did, they don't always depend on external services.

@babichjacob
Copy link
Member

2. During build `hook.ts` is executed. This can be a problem if your build environment isn't qualified to initialize external services.

Wrap the stuff you don't want to happen during build with if (!prerendering) https://kit.svelte.dev/docs#modules-$app-env

@ignatiusmb
Copy link
Member

I've just spent an unnecessarily long time with a certain problem that might be simpler if this was a thing, I was thinking to post this in #1530, but "hook for initialization" might be more appropriate. Can this be reconsidered as it would be more straightforward than the solution/workaround below?


The problem I was having is with an external library that provides an .init() function that can be filled with our own options and properties, everything that needs to happen before any user code is executed. There's no server.js or client.js anymore and instead (replaced with?) just hooks.js.

Now, I feel it's reasonable to assume that hooks are both server and client combined, so placing the .init() once in there should makes sense and in theory prepopulate the library options. But that isn't the case here and I've got to mess around inside node_modules itself to find out that the options that should be passed through .init() are still undefined in certain places of execution. This actually breaks SvelteKit in a way that cannot be debugged at all, it renders the page with everything for a second and then immediately throws an error, that's weirdly not caught by the ErrorLoad but shows the error page.

Inlining the options in the node_modules file itself works and successfully runs the app no problem, so it's not the library nor SvelteKit's fault, but changing the package through node_modules directly can't be considered a solution. I tried a lot more other ways in hooks to solve this but none seems to work.

I even tried to block off the whole handle hook from running before .init() function finishes, and at this point I realize that it might just not run at all. As a last resort, I duplicated all the initializing that's called in hooks and copied them to __layout, and everything works perfectly. Now, this is a huge problem because as the docs stated, it runs on both pages and endpoints, so it should've prepopulated the options without having to call them in __layout again.

This function runs on every request, for both pages and endpoints, and determines the response. It receives the `request` object and a function called `resolve`, which invokes SvelteKit's router and generates a response accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example).

@hgl
Copy link
Contributor Author

hgl commented Jun 3, 2021

@babichjacob Great tip! I totally forgot about build flags.

@ignatiusmb I believe hooks are server side only, "pages" in the referenced doc refers to their SSR step.

I do think hooks.ts's ability to function as the initiation point is not self-evident, might be worth to document it. I'll send a PR within the week if no one beats me to it.

@hgl
Copy link
Contributor Author

hgl commented Jun 4, 2021

Actually, I think I should ask before writing the doc:

Will the maintainers accept an PR that add a chapter about (server side) initialization to the doc? Should it be in its own chapter? Putting it in a subsection of the hooks chapter seems to assume it's obvious that initialization should happen in the hooks file, which I don't think is the case.

@ignatiusmb
Copy link
Member

Reopening to reconsider a single file approach and would probably resolve #1530 as well

@benmccann
Copy link
Member

I think it would be confusing if most hooks ran only on the server-side, but one ran on the client-side, so I wouldn't put something that runs on both in hooks. Today all hooks run on the server-only and for the most part if wouldn't make sense to run them on the client

There are certain things you want to do only on the server-side and I think hooks is a good place to do that. I wouldn't want to confuse that. E.g. initializing your database is something you would do only on the server-side

top-level await (which svelte kit doesn't seem to support)

This is a Node feature and you need to be running at least Node 14.8 for it to be present

Running code in __layout.svelte will happen on both the client and server if you want to have it happen in both places - though it's an imperfect solution if you're resetting the layout. We have #2169 open to track that.

@mzhang28
Copy link

mzhang28 commented Aug 29, 2021

I just want to add that what I'm looking for in an initialization hook is the ability to guarantee certain code finishes running before any of the rest of the server begins listening, since I don't want to wait until I receive a request to know that some of my initialization hasn't finished yet.

The promise in the handle hook only really works if setup is not expensive, otherwise the first request will take much longer than the rest. never mind I just realized what "next tick" meant

Also, it seems much more like a hack than something that SvelteKit was made to support.

@frederikhors
Copy link
Contributor

Let's say I'm using @urql/svelte and its initClient().

If I use that method both in

  1. src/routes/__layout.svelte and in
  2. src/routes/login/__layout.reset.svelte

it initializes my urql client two times!

Is there a workaround today?

I'm using it like this because in src/routes/__layout.svelte I redirect the user to /login if is not authenticated.

@rmunn
Copy link
Contributor

rmunn commented Dec 2, 2021

it initializes my urql client two times!

Is there a workaround today?

Here's an approach I just thought of.

Create a store in a separate module, say urqlclient.ts. It would look like this:

// urqlclient.ts
import { initClient } from 'svelte-urql';

const noop = () => {};

let client = null;
export const urqlclient = readable(null, (set) => {
  if (!client) {
    client = initClient('whatever params you need');
  }
  set(client);
  return noop;
});

Then when you need an URQL client, get it from the store via $urqlclient. The first time any piece of your code subscribes to this store, the readable's initialization function will be called. Since it calls set before returning, the first subscriber will receive a non-null value. If that first subscriber unsubscribes before a second subscriber asks for a client, the initialization function will be called again, but the second time it will simply return the cached value of client and not call initClient a second time.

This will allow the rest of your code to just do import { urqlclient } from '$lib/urqlclient.ts' and then use $urqlclient knowing that that value will be a properly-configured client.

@villetakanen
Copy link

villetakanen commented Dec 21, 2021

While the workaround (above) seems to clearly work - it requires a huge amount of typescript code for something that could be solved with one or three lines in other stacks like Vue3.

Or am I getting something wrong in this TS version

import { firebaseConfig } from '../env'
import { initializeApp } from 'firebase/app'
import type { FirebaseApp } from 'firebase/app'
import { readable } from 'svelte/store'
import type { Readable } from 'svelte/store'

// SvelteKit init for Firebase at the _Client side_

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {}
let app:FirebaseApp|null = null
export const firebaseApp:Readable<FirebaseApp> = readable(null, (set) => {
  if (!app) {
    app = initializeApp(firebaseConfig)
  }
  set(app)
  return noop
})

Almost the same block of readable init is required for other firebase modules, normally initiated with simple const auth = getAuth() or const analytics = getAnalytics().

Obviously, the question here is, how important the statically rendered SPA's are? After all, this can be done easily and in a lot more sensible manner at the server-side, if a static app bundle is not a requirement.

@Rich-Harris
Copy link
Member

Top-level await was broken in Vite at the time of #1538 (comment), but is since fixed, which means this problem is very easily solved — you can do your setup work in hooks.js...

const db = await createConnection();

export function handle({ request, resolve }) {
  request.locals.db = db;
  return resolve(request);
}

...or if that feels weird you can do it in a module that's imported by any endpoints that need it:

// src/lib/db.js
export const db = await createConnection();
// src/routes/my-endpoint.js
import { db } from '$lib/db';

export async function get() {
  return {
    body: await db.get(...)
  }
}

The latter technique also applies to clients that are needed by pages, like Urql. Demo here.

Given all this, is a dedicated initialization hook necessary?

@Mlocik97
Copy link
Contributor

Mlocik97 commented Jan 21, 2022

@Rich-Harris if there is some complicated logic at init, that takes some time, it would be better to do init immediately when server start instead when first request come, as it slows first request. But actually it doesn't need to be init hook, just execute hooks.js file at server start.

@rmunn
Copy link
Contributor

rmunn commented Jan 22, 2022

if there is some complicated logic at init, that takes some time, it would be better to do init immediately when server start instead when first request come, as it slows first request. But actually it doesn't need to be init hook, just execute hooks.js file at server start.

Alternately, you can start your server and immediately do a request to trigger the init logic: node server.js in one terminal, then curl http://example.com/request/ in another terminal. Might not work for every use case, but would work for all the use cases I've encountered personally.

@tv42
Copy link

tv42 commented Jan 26, 2022

@Mlocik97 An ICMP Echo Request is not even visible to a web server.

@Mlocik97
Copy link
Contributor

oh right, I meant to do GET command, somehow wrote bad one.. but it can be done same way:

exec(`GET ${process.env.server_url}`)

or even better, use "poststart" in package.json with GET command... You can create endpoint, for that, that will return nothing, just status, so You don't burden server.

@sinbino
Copy link

sinbino commented Mar 29, 2022

@Rich-Harris
In the latest version (create-svelte version 2.0.0-next.127), using top-level-await in hooks.js causes an error in the "svelte-kit build".

rendering chunks (7)...[vite:esbuild-transpile] Transform failed with 1 error:
index.js:2936:0: ERROR: Top-level await is not available in the configured target environment ("es2020")

Is there a workaround?

@Rich-Harris Rich-Harris added this to the 1.0 milestone Apr 26, 2022
Rich-Harris added a commit that referenced this issue Apr 26, 2022
* default to target: node16 - fixes #1538

* Update packages/kit/src/core/build/build_server.js

* Update .changeset/hungry-coats-sort.md

Co-authored-by: Conduitry <git@chor.date>

Co-authored-by: Conduitry <git@chor.date>
@MaaartinW
Copy link

Given all this, is a dedicated initialization hook necessary?

yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.