Created
June 22, 2023 16:40
-
-
Save SalathielGenese/bc866020687776779d55acc74a5834cb to your computer and use it in GitHub Desktop.
Override Angular's ApplicationRef.isStable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {ApplicationRef, DestroyRef, Injectable} from "@angular/core"; | |
import {SanityClient} from "@sanity/client"; | |
import {BehaviorSubject, debounceTime, filter, map, mergeMap, Observable, of, ReplaySubject, skip, tap} from "rxjs"; | |
import {LocaleService} from "./locale.service"; | |
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; | |
import {fromPromise} from "rxjs/internal/observable/innerFrom"; | |
@Injectable() | |
export class I18nService { | |
readonly #keys$ = new BehaviorSubject([] as string[]); | |
readonly #pendingCount$: ReplaySubject<number> = new ReplaySubject<number>(); | |
readonly #cache$ = new BehaviorSubject({} as TranslationMap); | |
constructor(destroyRef: DestroyRef, | |
sanityClient: SanityClient, | |
localeService: LocaleService, | |
applicationRef: ApplicationRef) { | |
const isStable$ = applicationRef.isStable; | |
Reflect.defineProperty(applicationRef, 'isStable', { | |
...Reflect.getOwnPropertyDescriptor(applicationRef, 'isStable'), | |
value: isStable$.pipe(mergeMap(_ => { | |
let current: undefined | number; | |
this.#pendingCount$.subscribe(_ => current = _).unsubscribe(); | |
return 0 === current ? of(_).pipe(debounceTime(1)) : of(false); | |
})), | |
}); | |
localeService.localeCode$ | |
.pipe(takeUntilDestroyed(destroyRef)) | |
.subscribe(() => this.#keys$.next([ | |
...this.#current(this.#keys$)!, | |
...Object.keys(this.#current(this.#cache$)!), | |
])); | |
localeService.localeCode$ | |
.pipe(mergeMap(localeCode => | |
this.#keys$.pipe(map(keys => [localeCode, keys] as const)))) | |
.pipe(takeUntilDestroyed(destroyRef)) | |
.pipe(filter(_ => !!_[1].length)) | |
.pipe(debounceTime(50)) | |
.pipe(mergeMap(([localeCode, keys]) => { | |
for (const key of keys) this.#cache$.value[key] = undefined; | |
return fromPromise(sanityClient | |
.fetch<I18nTranslation[]>(` | |
*[_type == "i18nTranslations" && !(_id match "drafts.") && locale->code == $localeCode && key->key in $keys] { | |
"key": key->key, | |
value | |
}`, {keys: keys.splice(0), localeCode})); | |
})) | |
.pipe(map(_ => _.reduce((__, _) => ({ | |
...__, | |
[_.key]: _.value, | |
}), this.#cache$.value))) | |
.pipe(tap(this.#cache$.next.bind(this.#cache$))) | |
.subscribe(() => setTimeout(() => | |
this.#pendingCount$.next(this.#current(this.#pendingCount$)! - 1))); | |
} | |
fetch(...keys: (string | undefined | null)[]): Observable<TranslationMap> { | |
const cache = this.#cache$.value; | |
const pruned = keys.filter(_ => _) as string[]; | |
const missing = pruned.filter(_ => !(_ in cache)); | |
missing.length && !this.#keys$.value.length | |
&& this.#pendingCount$.next((this.#current(this.#pendingCount$) ?? 0) + 1); | |
missing.length && this.#keys$.next([...missing, ...this.#keys$.value]); | |
const pending = pruned.filter(_ => undefined === cache[_] && _ in cache); | |
return this.#cache$ | |
.pipe(skip((missing.length ? 1 : 0) + (pending.length ? 1 : 0))) | |
.pipe(map(() => pruned.reduce((__, _) => ({ | |
...__, | |
[_]: this.#cache$.value[_], | |
}), {} as TranslationMap))); | |
} | |
#current<T>(observable: Observable<T>): T | undefined { | |
let current: T | undefined; | |
observable.subscribe(value => current = value).unsubscribe(); | |
return current; | |
} | |
} | |
export type TranslationMap = Record<string, undefined | string | null | never>; | |
export interface I18nTranslation { | |
value: string | null; | |
key: string; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For the context, I use Sanity.io for I81n.
But you should check for the free tier quota. Here
So I tought it would be great to throtle my queries to it and run them in batches.
Fortunately, it does work on the backend (SSR) where my content is rendered without translation (not good for SEO).
Moreover, it cause a glitch on the browser between SSR response and when the browser gets its own i18n translations.
So the idea is to increment the batches count issued and decrement when they complete and mork the application (well, this seervice) as stable only when that count is
0
(zero).But Angular SSR is even faster than that so I didn't use a
but aBehaviourSubject
ReplaySubject
to avoid initial values.