Despite the terrible name, I've had a lot of fun using Node's AsyncLocalStorage to manage stuff that traditionally either had to be global, passed in as an argument or awkwardly mushed into a [factory initialiser model](https://github.com/sap-labs-france/ev-server/blob/master-qa/src/monitoring/MonitoringServerFactory.ts) that doesn't quite suit Node. If you're familiar with thread-local storage, React context or request middleware then this will make sense. You can read more here about the API itself. This is also available in Workers, Bun and Deno.
I wrap it in a little type-safe syntactical sugar that really makes it seem like React which I prefer in my projects.
```ts
export const createStore = <T>(name: string) => {
const store = new AsyncLocalStorage<T>();
return [
<R>(value: T, cb: () => R) => store.run(value, cb),
() => {
const value = store.getStore();
if (value === undefined) {
throw new Error(`No value in store: "${name}"`);
return value;
}
}
] as const;
};
const [withStore, useStore] = createStore<Type>('name')
```
Any code that’s ran in the function or closure called with the `withStore` function can access the information in the store by using the `useStore` function. The game changer is that the `useStore` function can be used at ANY point down the call stack, not just in this function. Like context in React, it's a perfect replacement for prop-drilling or globals.
```ts
const sayHello = () => {
const { name } = useStore()
return `Hello ${name}!`
}
const value: Type = { name: 'James' };
return withStore(value, () => {
return sayHello();
})
```
# Environment
Environment variables & clients to databases, telemetry, storage, etc are a pretty perfect fit for this API. I've found them useful in a handful ways:
### Validation & derived variables
Using `process.env` itself can lead to a situation where you validate the variables when they're used, not on startup. This can hide configuration and deployment problems, which are never fun to debug. Even if you do validate process.env itself on startup, the behaviour isn't as you'd expect due to the requirement that values of process.env must always be keys, so you can't do something like this.
```ts
const EnvironmentSchema = z.object({
port: z.coerce.number()
flags: z.string().transform(s => s.split(','))
})
process.env = EnvironmentSchema.parse(process.env)
```
### Initialisation
In a global world, initialising clients asynchronously is awkward to do right, e.g. waiting for a database to connect, getting a connection string from a secret store, etc.
```ts
const client: Promise<Client> | undefined
export const getClient = async () => {
if (client) return client;
// Caching promise to prevent
// multiple inits while waiting
client = initClient()
.catch(error => {
console.error(error)
client = undefined;
})
return client;
}
const initClient = async () => {
const client = new Client();
await client.init();
return client;
}
```
Yucky stuff - and a weird process to understand so bugs & race conditions can happen really easily. That promise caching trick and that busting the cache on an error were embarrassing bug fixes to make.
The other option to this was to do this once at server start up and then somehow pass it all down as parameters to the functions that need it. That's fine for simple projects, if frustrating to include it in the parameters for every function call, but the moment you get into managing requests or managing state you end up finding places to stuff it like request contexts or global variables.
Now however, “somehow passing it down” is solved by this API, so this is how I handle that now:
```ts
export const config = z.object({
ENV_VAR: z.string(),
ENV_VAR_NUM: z.coerce.number()
}
const services = async (variables: z.infer<typeof config>) => {
const client = new Client();
await client.init();
return { client };
}
export const createEnvironment = async () => {
const variables = config.parse(process.env);
const services = await services(variables);
return {
variables,
...services
}
};
```
Then I can just wrap the rest of my code in it!
```ts
import { createEnvironment, withEnvironment } from './utils/env/env.js';
const environment = await createEnvironment();
await withEnvironment(environment, async () => {
const server = fastify(options);
await server.listen({ port: Number(process.env.PORT), host: process.env.HOST }, async (err, address) => {
if (err) {
console.error(err);
process.exit(1)
}
console.log('Running...')
});
});
```
Anything "downstream", in functions, request handlers, closures, etc. can super easily access all of those variables and clients.
```ts
server.get('/', async (req, res) => {
const { client } = useEnvironment();
return await client.foo();
})
```
### Multi-tenancy