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

Expose a way to inject a start script into adapter-node #927

Open
Prinzhorn opened this issue Apr 8, 2021 · 63 comments · Fixed by #13103
Open

Expose a way to inject a start script into adapter-node #927

Prinzhorn opened this issue Apr 8, 2021 · 63 comments · Fixed by #13103
Labels
Milestone

Comments

@Prinzhorn
Copy link

Prinzhorn commented Apr 8, 2021

Is your feature request related to a problem? Please describe.

I'm tying to migrate from Sapper to SvelteKit. I'm aware of the discussion in #334 and hooks seem to solve that.

I don't think there is currently a way to have code that runs on startup with the Node adapter. There are things that need to happen once and not for every request. In Sapper my server.js looked like this:

import sirv from 'sirv';
import express from 'express';
import compression from 'compression';
import bodyParser from 'body-parser';
import * as sapper from '@sapper/server';
import { Model } from 'objection';
import knex from './lib/knex.js';
import env from './lib/env.js';

Model.knex(knex); // <==============================================

const app = express();

app.use(compression({ threshold: 0 }));
app.use(sirv('static', { dev: env.isDev }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(sapper.middleware());

(async () => {
  await knex.migrate.latest({  // <==============================================
    directory: './src/migrations',
  });

  app.listen(env.PORT, (err) => {
    if (err) {
      console.error(err);
    }
  });
})();

The first line I've marked is something that would probably not hurt if done in every hook (setting up my Objection models with the knex instance).

The second line I've marked is running the database migration. In a serverfull environment this is a rather common occurrence. I want to run the migration right before starting the server so that I'm in a consistent state between database and application code. Even if this is scaled across multiple instances it works, since the second time the migrations run it's a NOOP. I honestly have no idea how people do that in a serverless environment with a guarantee that no code is run that expects an outdated schema resulting in possibly undefined behavior.

Describe the solution you'd like

A way to run setup code before the server starts. Maybe an async function setup() {} that is awaited before the server starts? But that can't work with every adapter.

Describe alternatives you've considered

I guess some will argue I should have a second API server separated from SvelteKit and whatnot. But I want less complexity, not more.

How important is this feature to you?

I cannot migrate to SvelteKit, unless I'm missing something.

@bjon
Copy link
Contributor

bjon commented Apr 8, 2021

Why don't not just run it before starting SvelteKit? Like node db-migration.js && node [SvelteKit]?

@Prinzhorn
Copy link
Author

Prinzhorn commented Apr 9, 2021

Why don't not just run it before starting SvelteKit? Like node db-migration.js && node [SvelteKit]?

But I want less complexity, not more.

I can't be the only one that needs a way to setup the environment for my application and would like a central place for that?

E.g. things like this

// I _think_ the aws-sdk can do this by itself using env variables, so it might be a bad example.
require('aws-sdk').config.update({
	accessKeyId: process.env.AWS_ACCESS_KEY_ID,
	secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
	region: 'eu-west-1'
});

or

if (process.env.NODE_ENV !== 'development') {
  // This will make the fonts folder discoverable for Chrome.
  // https://github.com/alixaxel/chrome-aws-lambda/blob/eddc7fbfb2d38f236ea20e2b5861736d3a22783a/source/index.js#L31
  process.env.HOME = '/tmp';
  fs.ensureDirSync('/tmp/.fonts');
  fs.copySync(path.join(__dirname, 'fonts'), '/tmp/.fonts');
}

I could put each of them in a module so importing would execute it once. But where would I import it if there is no central place? I guess I could abuse the hooks.js for that (outside of the actual hook functions), which works for setup code that is synchronous.

I also ran into a case where a module had to be the very first thing that I required (lovell/sharp#860 (comment)) but SvelteKit does not give me that type of flexibility.

Another idea

So what if the node-adapter could have a flag that would make index.js not polka() on it's own, but it exports a single function that I can use? That way I can import index.js, do all the things I want and then start the server.

@Prinzhorn
Copy link
Author

Prinzhorn commented Apr 9, 2021

So what if the node-adapter could have a flag that would make index.js not polka() on it's own, but it exports a single function that I can use? That way I can import index.js, do all the things I want and then start the server.

Uh, maybe I'll just lazily require('index.js') await import('./index.js') in my own entry script after setup is done 🎉

@Prinzhorn
Copy link
Author

Another real-world example: fetching metadata once during startup before launching the server https://docs.digitalocean.com/products/droplets/how-to/provide-user-data/

I'll leave this open to get some more feedback

@mankins
Copy link

mankins commented Apr 27, 2021

So what if the node-adapter could have a flag that would make index.js not polka() on it's own, but it exports a single function that I can use? That way I can import index.js, do all the things I want and then start the server.

You could always create your own adapter, using adapter-node as a guide.

I wanted to remove the console.log on server start (and add pino), so made a quick express based adapter that's easy to modify. I agree, it would be nice to have a convention for doing things to the server startup.

@Conduitry
Copy link
Member

Does top-level code in hooks.js get run? I would guess it probably would. You could put stuff there that you want to run once on server init and not per request.

@Prinzhorn
Copy link
Author

Prinzhorn commented Apr 29, 2021

You could always create your own adapter, using adapter-node as a guide.

Thanks for the hint, that sounds reasonable. I'll look into your Express adapter when I start working on this again.

You could put stuff there that you want to run once on server init and not per request.

I'd prefer not to introduce race-conditions and wait for the initialization to succeed before starting the server. I might even need some of the initialization logic to decide on certain parameters for the server. E.g. querying the hostname via cloud metadata service to configure CORS middleware. There are probably countless more use-cases.

@aewing
Copy link

aewing commented May 4, 2021

I brought this up back in December, and I still think when we load the adapter in kit we should look for and await an init function, then the adapter can use that however it wants to (if at all). In the case of the node adapter, it could execute an async method passed to the adapter from svelte.config.js and we could bootstrap our pools and whatnot there without making things weird for other adapters.

I think it would be cool if the init function could pass back an initial context state for each request.

@aewing

This comment was marked as duplicate.

@ValeriaVG
Copy link

ValeriaVG commented Sep 12, 2021

I use a hack-around for my SvelteKit TypeScript Node project:

// Create a promise, therefore start execution
const setup = doSomeAsyncSetup().catch(err=>{
  console.error(err)
  // Exit the app if setup has failed
  process.exit(-1)
})

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
   // Ensure that the promise is resolved before the first request
   // It'll stay resolved for the time being
    await setup;
    const response = await resolve(request);
    return response
}

Here's how it works:

  • App loads hooks and starts executing the promise
  • Until setup is complete all the requests are waiting
  • If the setup fails - the app exits with an error
  • Once setup is complete every request just jumps over await setup as it has already been resolved

Of course, having a setup hook would be cleaner and more straightforward than this. 👍

@dummdidumm
Copy link
Member

Related to #1538

@livehtml
Copy link

@ValeriaVG big thanks for your suggestion!
Can you please also show example for doSomeAsyncSetup function?

@ValeriaVG
Copy link

@livehtml Sure

const doSomeAsyncSetup = async ()=>{
   await connectToDB()
   await runMigrations()
   await chantASpell()
   await doWhateverElseYouNeedAsyncSetupFor()
}


// Create a promise, therefore start execution
const setup = doSomeAsyncSetup().catch(err=>{
  console.error(err)
  // Exit the app if setup has failed
  process.exit(-1)
})

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
   // Ensure that the promise is resolved before the first request
   // It'll stay resolved for the time being
    await setup;
    const response = await resolve(request);
    return response
}

@parischap
Copy link

parischap commented Oct 8, 2021

Thanks ValeriaVG for this clever code which has been very useful.

As pointed out in another thread, one issue with this solution is that the hook.js/ts file only gets loaded on first client request. And also server initialization shouldn't be in the handle hook.

So relying on your proposal and mixing it with mankins proposal to create a specific adapter, we could do the following:

Create an initialize.js/ts file that handles your initializations:

import { browser} from '$app/env';

const initializeAppCode = async () => {
  console.log('Client and server initialization');
  // Some global initializations here
  if (browser) {
    console.log('Client initialization');
    // Some client-side initializations here
  } else {
    console.log('Server initialization');
    // Some server-side initializations here
  }
};

// Start initializations as soon as this module is imported.  However, vite only loads
// the hook.js/ts module when needed, i.e. on first client request. So import this module from our own server in production mode
const initializeApp = initializeAppCode().catch((err) => {
  console.error(err);
  // Exit the app if setup has failed
  process.exit(-1);
});

export {initializeApp};

Then create a server.js/ts file in your src directory that calls your initializations:

import { path, host, port } from './env.js';
import { assetsMiddleware, kitMiddleware, prerenderedMiddleware } from './middlewares.js';
import compression from 'compression';
import polka from 'polka';
import '$lib/local_libs/initialize';  // <---- IMPORTANT LINE

const server = polka().use(
  // https://github.com/lukeed/polka/issues/173
  // @ts-ignore - nothing we can do about so just ignore it
  compression({ threshold: 0 }),
  assetsMiddleware,
  kitMiddleware,
  prerenderedMiddleware,
);

const listenOpts = { path, host, port };

server.listen(listenOpts, () => {
  console.log(`Listening on ${path ? path : host + ':' + port}`);
});

export { server };

Modify the svelte.config.js to tell node-adapter to use our server instead of the default one:

const config = {

  kit: {
    adapter: node({entryPoint: 'src/server.ts' }),  // You can pass other options if needed but entryPoint is the crucial part here
}
}

Then modify the handle hook like you proposed:

import type { Handle} from '@sveltejs/kit';
import { initializeApp } from '$lib/local_libs/initialize';

const handle: Handle = async function ({ request, resolve }) {
  // This will wait until the promise is resolved. If not resolved yet, it will block the first call to handle
  // but not subsequent calls.
  await initializeApp;

  const response = await resolve(request);
  return response;
};

There are two reasons for adding the await initializeApp; in the handle hook:

  • first, it will block any request until the server is fully initialized;
  • when in devmode (i.e. using npm dev), the server.ts file does not get called. So in dev mode, initialization is performed on first client request thanks to the import { initializeApp } from '$lib/local_libs/initialize'; in the hook.js/ts file.

Unfortunately, I could not come up with an elegant solution for client-side initialization. So this remains a workaround. A global init.js/ts file would still be appreciated.

@Prinzhorn
Copy link
Author

I'm still subscribed to this issue (d'oh, it's my own). Wasn't this recently solved? You can now use entryPoint and do all the things you've always done with Express/Polka and include SvelteKit as a set of middlewares.

I haven't worked on the SveliteKit migration since opening this issue, but looking at my original example I should now be able to do literally the same thing I did in Sapper.

I can't speak for other adapters, but for adapter-node I don't see an issue any longer (in before I actually try to migrate to entryPoint and run into issues 😄 ). This is definitely much cleaner than doing any of the above workarounds using some Promise-spaghetti.

@parischap
Copy link

Take some time to read carefully my answer. In my opinion, the Promise Spaghetti (as you call it) is still needed, even after defining a new entryPoint. And, so far, we have no clean solution in dev mode and no solution at all for client-side initialization.

I'm afraid we're in for pasta for a bit longer...

@Prinzhorn
Copy link
Author

Prinzhorn commented Oct 8, 2021

@martinjeromelouis gotcha, please add syntax highlighting to your code blocks in the future, it's hard to read 👍 . I don't think I fully understand the problems you're having with hooks. Aren't hooks entirely unrelated? What I want to do is run initialization before even calling server.listen, no need for magic imports.

This works as custom entry point:

import { assetsMiddleware, prerenderedMiddleware, kitMiddleware } from '../build/middlewares.js';
import init from './init.js';
import polka from 'polka';

const app = polka();

app.use(assetsMiddleware, prerenderedMiddleware, kitMiddleware);

init().then(() => {
  app.listen(3000);
});

The only thing missing is an option in Vite that allows injecting code before starting the server. But what keeps us from monkey patching listen inside configureServer?

This works during dev (where it's not critical to me that init finishes before listening):

import adapter from '@sveltejs/adapter-node';
import init from './src/init.js';

const myPlugin = {
  name: 'init-when-listen',
  configureServer(server) {
    const listen = server.listen;

    server.listen = (port, isRestart) => {
      init();
      return listen(port, isRestart);
    };
  }
};

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    // hydrate the <div id="svelte"> element in src/app.html
    target: '#svelte',
    adapter: adapter({
      entryPoint: './src/server.js'
    }),
    vite: {
      plugins: [myPlugin]
    }
  }
};

export default config;

@parischap
Copy link

@Prinzhorn Thanks for the listen monkey patching. Of course this is no persistent solution but I tried to implement it and ran into the following issue : as svelte.config.js is a js file, I cannot import my init.ts module. But still it can be of some help to people who write plain javascript.

Regarding the import of the init module in your entryPoint (prod), your solution can be improved by ValeriaVG Promise proposal as I showed in the example code above. In your code, the call to the init function blocks the server until initialization is fully performed which can be annoying if the init function depends for instance on external apis that take some time to respond. With the Promise trick, you start the init asynchronously and just keep the first request pending. Of course, this only matters when you have a website with a lot of traffic.

Finally, all this does not look like a definitive solution. A regular init.js/ts file that would automatically be called by Sveltekit both at client and server start would be a much cleaner solution.

@Prinzhorn
Copy link
Author

@martinjeromelouis I agree, it's a workaround and not a clean solution. I'm honestly overwhelmed by the complexity of SvelteKit, which is why I'm hesitant to adopt it at all. But that's a different story.

@andykais
Copy link

@Prinzhorn I tried your solution too. It works while svelte kit is in dev mode, which is great. However it is ignored entirely when building for adapter-node. Also as stated above, its outside the rest of the build process of sveltekit, so changes are not going to restart the. server, and preprocessing for things like typescript are out of the question.

I should mention I am not criticizing your design, just pointing out again the need for an official solution here.

@andykais
Copy link

perhaps the easiest solution would be to add an option to avoid lazy loading the hooks.js file? We could of course have a separate src/startup.js file which is loaded right away too. All this to say for most of us, we could get away with loading some data inside a module as singleton data, rather than complicating the process of a startup script which needs to pass args to the handle method

@Prinzhorn
Copy link
Author

@andykais I think you missed the first code block, which is the custom entry point for adapter-node that uses the same init.js that is used in dev. I made the example in response to the new entryPoint option to offer another workaround. But I absolutely agree that this is not the way to go moving forward.

@Xananax
Copy link

Xananax commented Feb 18, 2022

entrypoint isn't always sufficient;

My build scripts need access to Svelte's context. For example:

  • I heavily preprocess and generate a lot of metadata for Markdown (mdsvex) files that are collected with import.meta.glob.
  • I generate a tag list from all those files, that needs to be cached
  • I collect all product's descriptions, which are yaml'd in Markdown frontmatter, and upload the descriptions to Stripe to create new products.
  • etc...

I started with a set of external typescript files, which would get compiled, then ran, but keeping Typescript configs and such in sync was a bother. Slight environment differences would eat a lot of debugging time. So instead, building on the handle thing, I have something a bit different:

// routes/init.ts
import type { RequestHandler } from '@sveltejs/kit'

let hasInitiated = false

export const get: RequestHandler = async () => {
  const body = hasInitiated ? true : await initSequence
  return { body }
}

const initSequence = (async (): Promise<boolean> => {
  /**
   * Do the init stuff here
   */
  return new Promise((ok) => setTimeout(ok, 3000, true))
})()

initSequence.then(() => (hasInitiated = true))

On first run, I do test $(curl -sf http://localhost:3000/init) = "true" || kill $(pgrep node)

It's rough, but simple and sufficient for my needs. Maybe it gives someone inspiration.

@Rich-Harris
Copy link
Member

I'll confess I'm not totally sure what the feature request is here — between creating a custom server and doing setup work in src/hooks.js (where you can freely use top-level await, by the way — really helpful for this sort of thing) what is missing? (Yes, src/hooks.js doesn't get executed until the first request — though we could probably change that if necessary — but you could always just make a dummy request, even in the server itself.)

Would love to close this issue if possible!

@Rich-Harris Rich-Harris added this to the whenever milestone Apr 23, 2022
@andykais
Copy link

@Rich-Harris I think to put it concisely, we want the ability to run arbitrary code before the server starts. The general reason for this is:

  1. startup errors (like connecting to a database) are caught right away, rather than after your first user tries connecting to the server
  2. no lazy loading on the first response. This is similar to the first point, but it deals with user experience. If I have to load a db, a bunch of image resources, connect to aws and whatever else the first time the hook is called, then the first user to hit my app is going to have an extremely slow response, maybe even a timeout. I would much rather do all my initialization before exposing my app to the public network.

For me personally, I am using sveltekit to build a cli web app. So you can imagine there are cases where a user passes some flags to the app which are invalid (heck, parsing args is a bit of a pain via hooks right now as well). So their workflow looks like: 1. run the app on the cli, 2. open their browser, 3. see an error, 4. go back to the terminal and cancel and restart the app with different params. I also have no ability to run something like a --help command in front of my server being started

@mellanslag-de
Copy link

ah, great to know. I'll need this feature soon, so you most probably save me a lot of headache too, thanks you for trying it out!

@andykais
Copy link

andykais commented Dec 15, 2022

maybe the discussion now is just around making the dev server and production server have shared behavior?

[edit] it appears that src/hooks.server.ts now runs on dev server startup as well. Maybe we can close out this issue?

@mellanslag-de
Copy link

@andykais You mean it was just fixed within the last 2 days? Which version did you try out?

@andykais
Copy link

andykais commented Dec 16, 2022

@mellanslag-de looks like version 1.0.0 in node_modules/@sveltejs/kit/package.json (@sveltejs/kit': [email protected][email protected] in my pnpm.lock). Give it a try yourself and report back if its not working

@t1u1
Copy link

t1u1 commented Jan 6, 2023

it appears that src/hooks.server.ts now runs on dev server startup as well. Maybe we can close out this issue?

Yeah, it works for me too (sveltejs/kit 1.0.5).

But before closing the issue, it would be nice to have official handlers for both startup and shutdown. A shutdown handler would help with cleanup tasks.

@andykais
Copy link

andykais commented Jan 6, 2023

it would be nice to have official handlers for both startup and shutdown. A shutdown handler would help with cleanup tasks.

maybe we should start a separate issue for that? Personally I am interested in startup and shutdown triggers, I imagine you are talking about startup and shutdown hooks though. When does shutdown occur anyways? When the process is killed? You could probably detect SIGTERM signals and the like. I guess I am just pointing out that it might warrant some discussion, rather than just being tacked onto this issue

@t1u1
Copy link

t1u1 commented Jan 7, 2023

maybe we should start a separate issue for that? Personally I am interested in startup and shutdown triggers, I imagine you are talking about startup and shutdown hooks though.

Yes, please go ahead and create one. I have only just started experimenting with svelte, and I am not familiar with the distinction between trigger and hook.

When does shutdown occur anyways? When the process is killed? You could probably detect SIGTERM signals and the like. I guess I am just pointing out that it might warrant some discussion, rather than just being tacked onto this issue

Agreed, could be discussed. Some of us are hoping for an idiomatic and documented way to do these things, that will work consistently on all platforms where startup/shutdown can be supported.

@MiraiSubject
Copy link

it appears that src/hooks.server.ts now runs on dev server startup as well. Maybe we can close out this issue?

Yeah, it works for me too (sveltejs/kit 1.0.5).

'@sveltejs/kit': [email protected][email protected]: It absolutely does not run immediately on startup of the dev server for me. I only get my console.log in hooks.server.ts after the first request has been made.

My hooks.server.ts file is:

import { handleSession } from 'svelte-kit-cookie-session';
import { PRIVATE_COOKIE_SECRET } from '$env/static/private'

console.log("Hello?")

export const first = handleSession({
	secret: `${PRIVATE_COOKIE_SECRET}`
});

export const handle = first;

The specific file in question: https://github.com/MiraiSubject/oth-verification/blob/2254f1693dd48cbed46ccf40d186754398798bc7/src/hooks.server.ts

When booting up in production however by using node-adapter, and then running vite build and then node build/index.js, that console.log does run, which was already determined earlier in the thread.

@andykais
Copy link

I can confirm what @MiraiSubject has said, in the latest sveletkit ('@sveltejs/kit': [email protected][email protected]) hooks.server.ts does not appear to be imported until the first request is made. It seems like when this was working, it was not an intentional design decision.

@PeytonHanel
Copy link
Contributor

https://kit.svelte.dev/docs/hooks

Code in these modules will run when the application starts up, making them useful for initializing database clients and so on.

@Rich-Harris Does the Svelte team have a plan to resolve this issue since the documentation isn't clear as to what's happening?

@AngryLoki
Copy link

AngryLoki commented Feb 28, 2023

@PeytonHanel , hooks.server.js indeed runs when server starts up ...but only in production mode (i. e. npm run preview or node build or node <your custom launcher>). In npm run dev mode first of all, it starts only on first request, and another pain is that it does not integrate well with hot reload. For example, if you schedule an operation with setinterval, each hot reload will install an extra setinterval. The dirty workaround is to save something in global.<> and do cleanup manually.

As for correct approach... I don't think hooks.server.ts is suitable. We need a new interface, something like export const onStart = async () ={ ... } and export const onStop = async () ={ /* graceful stop on hot reload */ } instead of piggybacking on hooks.

@luca-vogels
Copy link

luca-vogels commented Mar 22, 2023

Coming from NextJS I would like to see that hot-reloads are handled by the request handler provided by @sveltejs/adapter-node:

// Current SvelteKit request handler does not support hot-reloads
import express from "express";
import { handler } from "../build/handler.js";

const app = express();
app.use(handler); // add hot-reload here!
app.listen();

Currently, this design requires a manual build and restart of the server in order to see component changes.
Very painful! This is the reason why I'll stay at NextJS!
In my opinion, not providing an easy way to properly integrate the framework into other servers/apps is devastating!

The logic for hot-reloads can definitely be embedded into the request handler as seen in NextJS:

// NextJS Request Handler that supports hot-reloads
import express from 'express';
import next from 'next';

const nextApp = next({dev}); // tell if dev or production
await nextApp.prepare(); // initial build
const nextHandler = nextApp.getRequestHandler(); // request handler with hot-reload integrated

const app = express();
app.use(nextHandler); // watches and hot-reloads components if needed (only in dev mode)
app.listen();

@flawnn
Copy link

flawnn commented Apr 20, 2023

I'm confused why this isn't a more important issue. There should be an easy way to call code on app startup both in dev mode and when built. Relying on a first request to do initialization doesn't make any sense.

Sorry to ping but has there been any working solution to this?
I had found (vite-test-utils)[https://github.com/kazupon/vite-test-utils], which provided quite a nice way of starting SvelteKit programmatically (which also when googling for a solution, is nowhere to be found and asked). This might be a dirty workaround for running own stuff O.o

But still this also touches on another issue, that goes hand-in-hand with this one: How can we run the server over some Node-API? I want to be able to mock stuff in the server which needs to run obviously in the same process for that. Currently, I reside in simulating requests to the respective API handler functions.

but only in production mode (i. e. npm run preview

That's also not working over npm run preview. How can this be so hard to have some code running on startup?

@lazy-detourer
Copy link

I also hope this issue gets resolved.
It's strange that it doesn't work only on dev runs.
I'm even more disappointed with this, as I'm impressed with svletekit's developer-friendly features.

@dmoebius
Copy link

dmoebius commented Jun 13, 2023

Just as @robpc I had the requirement to start a websocket handler (Socket.io in this case) together with the Polka server instantiated by node-adapter. I found the solution proposed by robpc unsatisfying, because it means running npm package each time a server change is made.

I realized that hooks.server.ts is the only viable solution to initialize anything for the three environments dev, preview & production. The only problem is that in hooks.server.ts there's no access to the http.Server instance which is needed to setup Socket.io. So I used a global for that. 😏

I also wanted to use the original startup code generated by adapter-node as much as possible, because it contains important setup. I don't want to replace with a custom handler, because then I would no longer benefit from updates etc. A wrapper around the generated code is ok, though.

All in all my solution looks like this:

For the production code generated to build/ I wrote a wrapper:

// start.js
global.httpServer = new Promise((resolve) => {
    import('./index.js').then(index => resolve(index.server.server))
});

This script starts the server (by importing the generated index.js) then injects the HTTP Server into a global as a Promise. start.js is copied to the build/ directory at the end of the build and acts as the new starting point.

In hooks.server.ts I added the following:

import {startSocketIOServer} from '$lib/server/socket/websocketServer'
...
startSocketIOServer();

where websocketServer.ts contains:

import {Server} from 'socket.io'

function startSocketIOServer() {
    global.httpServer?.then(httpServer => {
        const io = new Server(httpServer);
        io.on('connection', (socket) => {
            ... // do whatever should happen on a new connection
        })
        console.log('SocketIO configured')
    })
}

export {startSocketIOServer}

This works for production, ie. npm run build and then node build/start.js. For npm run dev and npm run preview to work as well, I added the following to vite.config.ts:

// vite.config.ts
import {sveltekit} from "@sveltejs/kit/vite"
import {defineConfig} from "vite"

export default defineConfig({
    plugins: [
        sveltekit(),
        {
            name: 'sveltekit-inject-httpserver',
            configureServer(server) {
                global.httpServer = Promise.resolve(server.httpServer!)
            },
            configurePreviewServer: {
                order: 'pre', // important so that the handler executes _before_ hooks.server.ts
                handler: (server) => {
                    global.httpServer = Promise.resolve(server.httpServer)
                },
            },
        },
    ],
    ...

That's all! 😒 Almost. To give global.httpServer the correct type definition, I also added:

// src/globals.d.ts
import http from 'node:http'

declare global {
    namespace globalThis {
        var httpServer: Promise<http.Server>
    }
}

It took me a whole day to figure this all out. It should be easier to add initialization code in SvelteKit.

IMHO hooks.server.ts should get a second hook besides handle : Handle. Maybe something like initServer(args). Ideally args would already contain the http.Server instance, but this of course depends on the used adapter. Maybe every adapter should be free to inject into args whatever they want.

Hope this helps someone. Initializing a websocket handler along with the SvelteKit server is not an uncommon requirement. Still waiting for #1491 though.

@UnlimitedBytes
Copy link

@dmoebius Thanks for the share of this great knowledge. I was about to create a SvelteKit adapter on base of adapter-node to solve this issue and some behavior I dislike (like including package.json in the build, etc.). This literally saved me a lot of time building the new adapter. Ofc. I will also credit you :)

@dmoebius
Copy link

@UnlimitedBytes Note that someone already went the route of creating a custom adapter, see https://github.com/carlosV2/adapter-node-ws

A short addendum to my suggestion above: during HMR in vite dev mode, the socket.io server sometimes gets added a second time, because startSocketIOServer() is executed again, leading to unpleasant results. This can be suppressed by adding another global variable:

function startSocketIOServer() {
    global.httpServer?.then(httpServer => {
        let io : Server;
        if (global.currentSocketIOServer) {
            io = global.currentSocketIOServer; // HMR: reuse previous instance
        } else {
            io = new Server(httpServer);
            global.currentSocketIOServer = io;
        }
        io.on('connection', (socket) => {
            ... // do whatever should happen on a new connection
        });
        console.log('SocketIO configured');
    })
}

@UnlimitedBytes
Copy link

UnlimitedBytes commented Jun 15, 2023

@dmoebius Thanks for the information I looked at the linked repo and actually could reuse some of the code for my adapter. I added both of you in the credits. If someone is interested I released a very early alpha build on https://github.com/UnlimitedBytes/sveltekit-adapter-custom. It's already capable of hijacking the http server in your hooks.server.ts file and adding http.Server middleware but currently doesn't support express like middleware.

Heres a quick example how to add a socket.io websocket server:

// hooks.server.ts
import type { SetupHook } from 'sveltekit-adapter-custom';

export const setup: SetupHook = async (httpServer) => {
	const io = new Server(httpServer);

	io.on('connect', (socket) => {
		console.log(`New user (${socket.client.conn.remoteAddress}) connected.`);

		socket.on('message', (message) => {
			io.emit('message', message);
		});

		socket.on('disconnect', () => {
			console.log(`User (${socket.client.conn.remoteAddress}) disconnected.`);
		});
	});
};

@janguardian
Copy link

I've been reading this entire thread for a while now and I do believe too that things should not be that complicated and that we need a simple solution. As a dirty yet easy workaround I added && wget http://localhost:5173 to my dev script. This did the job of running the code in the hooks.server.js file.

@kevinmungai
Copy link

I had a similar issue and I used vite itself to make an initial request to the server as soon as the server has initialized. This only works in development which is what I wanted. Maybe it could help someone in the future.

{
  name: 'start-up-request',
  configureServer(server) {
    server.httpServer?.on('listening', async () => {
      await fetch('http://localhost:5173').catch((e) => console.error(e));
    });
  }
}

@zicklag
Copy link

zicklag commented Sep 25, 2024

I also need this. My current solution is to just add a setup function to hooks.server.ts, but that only starts after the first request to the web-app which is not optimal because in my use-case the setup actually sets up a Discord bot, and that Discord bot could hypothetically be interacted with before the first request to my web app, after a restart of the server.

A custom server would work, but is otherwise unnecessary and means I would have to re-implement the existing functionality myself, when all I need is to be able to run a function at startup.

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

Successfully merging a pull request may close this issue.