Skip to content

Commit

Permalink
feat: added support for hydrate function
Browse files Browse the repository at this point in the history
  • Loading branch information
mbret committed Jul 16, 2024
1 parent d0affc3 commit d0e719d
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 43 deletions.
13 changes: 13 additions & 0 deletions src/lib/state/persistance/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type Signal } from "../signal"
import type { IDENTIFIER_PERSISTANCE_KEY } from "./constants"

export interface Adapter {
Expand All @@ -12,3 +13,15 @@ export interface PersistanceEntry {
migrationVersion?: number
[IDENTIFIER_PERSISTANCE_KEY]: typeof IDENTIFIER_PERSISTANCE_KEY
}

export interface SignalPersistenceConfig<Value> {
version: number
signal: Signal<any, Value, string>
/**
* Only called if there is a value to hydrate
*/
hydrate?: (params: {
version: number
value: Value
}) => Value
}
81 changes: 38 additions & 43 deletions src/lib/state/persistance/usePersistSignals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,31 @@ import {
from,
map,
merge,
mergeMap,
of,
switchMap,
tap,
throttleTime,
zip
} from "rxjs"
import { useSubscribe } from "../../binding/useSubscribe"
import type { PersistanceEntry, Adapter } from "./types"
import type { Signal } from "../signal"
import type {
PersistanceEntry,
Adapter,
SignalPersistenceConfig
} from "./types"
import { getNormalizedPersistanceValue } from "./getNormalizedPersistanceValue"
import { IDENTIFIER_PERSISTANCE_KEY } from "./constants"
import { isDefined } from "../../utils/isDefined"
import { useLiveBehaviorSubject } from "../../binding/useLiveBehaviorSubject"

const persistValue = ({
adapter,
signal,
version
config
}: {
adapter: Adapter
signal: Signal<unknown, unknown, string>
version: number
config: SignalPersistenceConfig<any>
}) => {
const { signal, version } = config
const state = signal.getValue()

const value = {
Expand All @@ -49,49 +50,55 @@ const persistValue = ({
)
}

const hydrateValueToSignal = ({
function hydrateValueToSignal<Value>({
adapter,
version,
signal
config
}: {
adapter: Adapter
version: number
signal: Signal<unknown, unknown, string>
}) => {
config: SignalPersistenceConfig<Value>
}) {
const { hydrate = ({ value }) => value, signal, version } = config

return from(adapter.getItem(signal.config.key)).pipe(
switchMap((value) => {
const normalizedValue = getNormalizedPersistanceValue(value)

if (!normalizedValue) return of(value)

if (
normalizedValue.migrationVersion !== undefined &&
version > normalizedValue.migrationVersion
(normalizedValue.migrationVersion !== undefined &&
version > normalizedValue.migrationVersion) ||
normalizedValue.value === undefined
) {
return of(value)
}

signal.setValue(normalizedValue.value)
const correctVersionValue = normalizedValue.value as Value

signal.setValue(hydrate({ value: correctVersionValue, version }))

return of(value)
})
)
}

export const usePersistSignals = ({
export function usePersistSignals({
entries = [],
onReady,
adapter
}: {
entries?: Array<{ version: number; signal: Signal<any, any, string> }>
entries?: Array<SignalPersistenceConfig<any>>
/**
* Triggered after first successful hydrate
*/
onReady?: () => void
/**
* Requires a stable instance otherwise the hydration
* process will start again. This is useful when you
* need to change adapter during runtime.
*/
adapter?: Adapter
}) => {
adapter: Adapter
}) {
const entriesRef = useLiveRef(entries)
const onReadyRef = useLiveRef(onReady)
const adapterSubject = useLiveBehaviorSubject(adapter)
Expand All @@ -102,30 +109,19 @@ export const usePersistSignals = ({

return adapterSubject.current.pipe(
switchMap((adapterInstance) => {
if (!adapterInstance) return of(false)

const stream =
entries.length === 0
? of(true)
: zip(
...entries.map(({ signal, version }) =>
...entries.map((config) =>
hydrateValueToSignal({
adapter: adapterInstance,
signal,
version
}).pipe(
mergeMap(() =>
persistValue({
adapter: adapterInstance,
signal,
version
})
)
)
config
})
)
).pipe(map(() => true))

return merge(of(false), stream).pipe(
return stream.pipe(
tap(() => {
if (onReadyRef.current != null) onReadyRef.current()
}),
Expand All @@ -152,25 +148,24 @@ export const usePersistSignals = ({
useSubscribe(
() =>
isHydratedSubject.current.pipe(
filter((value) => value),
filter((isHydrated) => isHydrated),
switchMap(() => adapterSubject.current),
filter(isDefined),
switchMap((adapterInstance) =>
merge(
...entriesRef.current.map(({ signal, version }) =>
signal.subject.pipe(
...entriesRef.current.map((config) =>
config.signal.subject.pipe(
throttleTime(500, asyncScheduler, {
trailing: true
}),
switchMap(() => {
return from(
switchMap(() =>
from(
persistValue({
adapter: adapterInstance,
signal,
version
config
})
)
})
)
)
)
)
Expand Down
3 changes: 3 additions & 0 deletions src/lib/state/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ export interface Signal<
export function signal<T = undefined, V = T>(
config?: Omit<Partial<Config<T, string | undefined>>, "key" | "get">
): Signal<T, V, undefined>

export function signal<T = undefined, V = T>(
config: Omit<Partial<Config<T, string | undefined>>, "get"> & {
key: string
}
): Signal<T, V, string>

export function signal<V = undefined>(
config: ReadOnlySignalConfig<V>
): ReadOnlySignal<V>

export function signal<
T = undefined,
V = undefined,
Expand Down

0 comments on commit d0e719d

Please sign in to comment.