Skip to content

Commit

Permalink
Add opt-out for AsyncLocalStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
LorisSigrist committed Jul 3, 2024
1 parent e5b6a3a commit 82581f7
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 43 deletions.
23 changes: 23 additions & 0 deletions .changeset/fast-stingrays-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@inlang/paraglide-sveltekit": minor
---

Adds a `disableAsyncLocalStorage` option to `i18n.handle`. This allows you to opt out of using the experimental `AsyncLocalStorage` API.

**Warning**
Disabling `AsyncLocalStorage` removes the protection against concurrent requests overriding each other's language state.

Only opt out if `AsyncLocalStorage` if you are certain your environment does not handle concurrent requests in the same process. For example in Vercel Edge functions or Cloudflare Workers.

In environments where only one request is processed in a given process disabling `AsyncLocalStorage` can yield performance gains.

**Example**
```ts
// src/hooks.server.js
import { i18n } from "$lib/i18n"

export const handle = i18n.handle({
disableAsyncLocalStorage: true // @default = false
})

```
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
"dedent": "1.5.1",
"devalue": "^4.3.2",
"magic-string": "^0.30.5",
"svelte": "^5.0.0 || ^5.0.0-next.1 || ^5.0.0-rc.1",
"unctx": "^2.3.1"
"svelte": "^5.0.0 || ^5.0.0-next.1 || ^5.0.0-rc.1"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Handle } from "@sveltejs/kit"
import type { I18nConfig } from "../adapter.server.js"
import type { RoutingStrategy } from "../strategy.js"
import type { ParaglideLocals } from "../locals.js"
import { createContext } from "unctx"
import { ALSContext, GlobalContext, type Context } from "./utils.js"

/**
* The default lang attribute string that's in SvelteKit's `src/app.html` file.
Expand Down Expand Up @@ -58,39 +58,43 @@ export type HandleOptions = {
* ONLY DISABLE THIS IF YOU ARE CERTAIN YOUR ENVIRONMENT DOES
* NOT ALLOW CONCURRENT REQUESTS.
*
* For example: Edge functions
* For example in Vercel Edge functions
*
* @default true
* @default false
*/
asyncLocalStorage?: boolean
disableAsyncLocalStorage?: boolean
}

export const createHandle = <T extends string>(
strategy: RoutingStrategy<T>,
i18n: I18nConfig<T>,
options: HandleOptions
): Handle => {
const shouldUseAsyncLocalStorage = options.asyncLocalStorage ?? true

let ALS = undefined
const languageContext = createContext<T>({
asyncContext: true,
AsyncLocalStorage: ALS,
})

i18n.runtime.setLanguageTag(() => {
const val = languageContext.tryUse()
return i18n.runtime.isAvailableLanguageTag(val) ? val : i18n.defaultLanguageTag
})
let languageContext: Context<T> | undefined = undefined
function initializeLanguageContext(
AsyncLocalStorage: typeof import("node:async_hooks").AsyncLocalStorage | undefined
) {
languageContext = AsyncLocalStorage ? new ALSContext(AsyncLocalStorage) : new GlobalContext()
i18n.runtime.setLanguageTag(() => {
if (!languageContext)
throw new Error(
"languageContext not initialized - This should never happen, please file an issue"
)
const val = languageContext.get()
return i18n.runtime.isAvailableLanguageTag(val) ? val : i18n.defaultLanguageTag
})
}

const langPlaceholder = options.langPlaceholder ?? "%paraglide.lang%"
const dirPlaceholder = options.textDirectionPlaceholder ?? "%paraglide.textDirection%"

return async ({ resolve, event }) => {
// make sure `node:async_hooks` has been loaded
if (shouldUseAsyncLocalStorage) {
const { AsyncLocalStorage } = await import("node:async_hooks")
ALS = AsyncLocalStorage
// if the langauge context is not yet initialized
if (!languageContext) {
const als = options.disableAsyncLocalStorage
? undefined
: (await import("node:async_hooks")).AsyncLocalStorage
initializeLanguageContext(als)
}

const [localisedPath, suffix] = parseRoute(event.url.pathname as `/${string}`, base)
Expand Down Expand Up @@ -140,6 +144,10 @@ export const createHandle = <T extends string>(
// @ts-expect-error
event.locals.paraglide = paraglideLocals

if (!languageContext)
throw new Error(
"languageContext not initialized - This should never happen, please file an issue"
)
return languageContext.callAsync(paraglideLocals.lang, async () => {
return await resolve(event, {
transformPageChunk({ html, done }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as runtime from "$paraglide/runtime.js"
import { PrefixStrategy } from "../strategy"

const reroute = createReroute<"en" | "de">(
PrefixStrategy(runtime.availableLanguageTags, runtime.sourceLanguageTag, {}, {})
PrefixStrategy(runtime.availableLanguageTags, runtime.sourceLanguageTag, {}, {}, "always")
)

describe("reroute", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type ALS<T> = import("node:async_hooks").AsyncLocalStorage<T>
export type ALSPrototype = typeof import("node:async_hooks").AsyncLocalStorage

export interface Context<T> {
get(): T | undefined
callAsync: <CB extends () => any>(val: T, cb: CB) => Promise<Awaited<ReturnType<CB>>>
}

export class ALSContext<T> implements Context<T> {
ctx: ALS<T>
constructor(ALS: ALSPrototype) {
this.ctx = new ALS<T>()
}

get(): T | undefined {
return this.ctx.getStore()
}

async callAsync(val: T, cb: () => any) {
return await this.ctx.run(val, cb)
}
}
export class GlobalContext<T> implements Context<T> {
value: T | undefined = undefined

get(): T | undefined {
return this.value
}

async callAsync(val: T, cb: () => any) {
this.value = val
return await cb()
}
}
75 changes: 55 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 82581f7

Please sign in to comment.