feat: add block picker to storage

This commit is contained in:
Victor Oliva
2025-11-14 00:24:23 -03:00
parent d4a741a046
commit 1ffc6c6c23
10 changed files with 336 additions and 173 deletions
+2 -2
View File
@@ -25,8 +25,8 @@ export const ButtonGroup: FC<{
key={value}
className={twMerge(
"text-secondary-foreground px-3 py-1",
"hover:text-polkadot-500",
"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
"hover:text-polkadot-500 font-light",
"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-bold",
disabled && "opacity-50 pointer-events-none",
)}
disabled={disabled}
+18 -9
View File
@@ -1,24 +1,24 @@
import { lookup$, runtimeCtx$ } from "@/state/chains/chain.state"
import { ViewCodec } from "@/codec-components/ViewCodec"
import { ButtonGroup } from "@/components/ButtonGroup"
import { DocsRenderer } from "@/components/DocsRenderer"
import { ExpandBtn } from "@/components/Expand"
import { LoadingMetadata } from "@/components/Loading"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { withSubscribe } from "@/components/withSuspense"
import { CodecComponentType } from "@polkadot-api/react-builder"
import { lookup$, runtimeCtx$ } from "@/state/chains/chain.state"
import { getTypeComplexity } from "@/utils/shape"
import { state, useStateObservable } from "@react-rxjs/core"
import { CodecComponentType } from "@polkadot-api/react-builder"
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
import { Dot } from "lucide-react"
import { HexString } from "polkadot-api"
import { FC, useState } from "react"
import { map } from "rxjs"
import { twMerge } from "tailwind-merge"
import { ValueDisplay } from "./Storage/StorageSubscriptions"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const metadataConstants$ = state(
lookup$.pipe(
@@ -154,11 +154,14 @@ const ConstantEntry: FC<{
)
}
const defaultedCtx$ = runtimeCtx$.pipeState(withDefault(null))
const ConstantValue: FC<{ type: number; decoded: unknown }> = ({
type,
decoded,
}) => {
const [mode, setMode] = useState<"json" | "decoded">("decoded")
const ctx = useStateObservable(defaultedCtx$)
if (!ctx) return null
return (
<div className="pl-6 py-2 flex flex-col gap-2 items-start overflow-hidden w-full">
@@ -177,7 +180,13 @@ const ConstantValue: FC<{ type: number; decoded: unknown }> = ({
]}
/>
<div className="overflow-auto w-full">
<ValueDisplay mode={mode} type={type} value={decoded} title="Value" />
<ValueDisplay
mode={mode}
ctx={ctx}
type={type}
value={decoded}
title="Value"
/>
</div>
</div>
)
+1 -1
View File
@@ -244,7 +244,7 @@ export const inMemoryBlocks$ = state(
)
inMemoryBlocks$.subscribe()
const blockHash$ = (hashOrHeight: string) =>
export const blockHash$ = (hashOrHeight: string) =>
hashOrHeight.length > 63
? of(hashOrHeight.startsWith("0x") ? hashOrHeight : `0x${hashOrHeight}`)
: client$.pipe(
@@ -1,5 +1,7 @@
import { PathsRoot } from "@/codec-components/common/paths.state"
import { ButtonGroup } from "@/components/ButtonGroup"
import { useStateObservable } from "@react-rxjs/core"
import { runtimeCtx$ } from "@/state/chains/chain.state"
import { useStateObservable, withDefault } from "@react-rxjs/core"
import { Trash2 } from "lucide-react"
import { FC, useState } from "react"
import { ValueDisplay } from "../Storage/StorageSubscriptions"
@@ -9,7 +11,6 @@ import {
runtimeCallResult$,
runtimeCallResultKeys$,
} from "./runtimeCalls.state"
import { PathsRoot } from "@/codec-components/common/paths.state"
export const RuntimeCallResults: FC = () => {
const keys = useStateObservable(runtimeCallResultKeys$)
@@ -71,10 +72,14 @@ const RuntimeCallResultBox: FC<{ subscription: string }> = ({
)
}
const defaultedCtx$ = runtimeCtx$.pipeState(withDefault(null))
const ResultDisplay: FC<{
runtimeCallResult: RuntimeCallResult
mode: "json" | "decoded"
}> = ({ runtimeCallResult, mode }) => {
const ctx = useStateObservable(defaultedCtx$)
if (!ctx) return null
if ("error" in runtimeCallResult) {
return (
<div className="text-sm">
@@ -92,6 +97,7 @@ const ResultDisplay: FC<{
<div className="max-h-[60svh] overflow-auto">
<ValueDisplay
mode={mode}
ctx={ctx}
type={runtimeCallResult.type}
value={runtimeCallResult.result}
title={"Result"}
+93
View File
@@ -0,0 +1,93 @@
import { Spinner } from "@/components/Icons"
import { runtimeCtx$, runtimeCtxAt$ } from "@/state/chains/chain.state"
import { RuntimeContext } from "@polkadot-api/observable-client"
import { Input } from "@polkahub/ui-components"
import { state, useStateObservable } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"
import { Check, X } from "lucide-react"
import {
catchError,
debounceTime,
filter,
map,
Observable,
startWith,
switchMap,
} from "rxjs"
import { blockHash$ } from "../Explorer/block.state"
const [valueChange$, setValue] = createSignal<string>()
const inputValue$ = state(valueChange$, "Latest")
const loadedCtx$ = state(
inputValue$.pipe(
debounceTime(200),
switchMap((v) => {
const inner$: Observable<{
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
hash?: string
}> =
!v.trim() || v === "Latest"
? runtimeCtx$.pipe(map((ctx) => ({ ctx })))
: blockHash$(v).pipe(
switchMap((hash) =>
runtimeCtxAt$(hash).pipe(map((ctx) => ({ ctx, hash }))),
),
)
return inner$.pipe(
map((value) => ({
type: "success" as const,
value,
})),
catchError((value) => [
{
type: "error" as const,
value,
},
]),
startWith({
type: "loading" as const,
}),
)
}),
),
{ type: "loading" as const },
)
export const selectedBlock$ = loadedCtx$.pipeState(
filter((v) => v.type === "success"),
map((v) => v.value),
)
export const BlockPicker = () => {
const value = useStateObservable(inputValue$)
const loadedCtx = useStateObservable(loadedCtx$)
return (
<div className="flex items-center">
<Input
placeholder="Block hash or number"
value={value}
onChange={(evt) => setValue(evt.target.value)}
onFocus={() => {
if (value === "Latest") {
setValue("")
}
}}
onBlur={() => {
if (!value.trim()) {
setValue("Latest")
}
}}
/>
{loadedCtx.type === "loading" ? (
<Spinner className="text-muted-foreground" />
) : loadedCtx.type === "error" ? (
<X className="text-red-500" />
) : (
<Check className="text-green-600" />
)}
</div>
)
}
+10 -5
View File
@@ -4,10 +4,11 @@ import { Chopsticks } from "@/components/Icons"
import { LoadingMetadata } from "@/components/Loading"
import { SearchableSelect } from "@/components/Select"
import { withSubscribe } from "@/components/withSuspense"
import { canSetStorage$, lookup$ } from "@/state/chains/chain.state"
import { canSetStorage$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC, useState } from "react"
import { map } from "rxjs"
import { BlockPicker, selectedBlock$ } from "./BlockPicker"
import { partialEntry$, selectedEntry$, selectEntry } from "./storage.state"
import { StorageDecode } from "./StorageDecode"
import { StorageQuery } from "./StorageQuery"
@@ -15,11 +16,11 @@ import { StorageSet } from "./StorageSet"
import { StorageSubscriptions } from "./StorageSubscriptions"
const metadataStorage$ = state(
lookup$.pipe(
map((lookup) => ({
lookup,
selectedBlock$.pipe(
map(({ ctx }) => ({
lookup: ctx.lookup,
entries: Object.fromEntries(
lookup.metadata.pallets
ctx.lookup.metadata.pallets
.filter((p) => p.storage)
.map((p) => [
p.name,
@@ -41,6 +42,10 @@ export const Storage = withSubscribe(
return (
<div className="p-4 pb-0 flex flex-col gap-2 items-start">
<div className="flex items-center gap-2">
<label>
Block
<BlockPicker />
</label>
<label>
Pallet
<SearchableSelect
+9 -6
View File
@@ -1,18 +1,20 @@
import { ActionButton } from "@/components/ActionButton"
import { dynamicBuilder$, runtimeCtx$ } from "@/state/chains/chain.state"
import { NOTIN } from "@polkadot-api/react-builder"
import { state, useStateObservable } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"
import { FC } from "react"
import { combineLatest, firstValueFrom, map, of } from "rxjs"
import { selectedBlock$ } from "./BlockPicker"
import { addStorageSubscription, selectedEntry$ } from "./storage.state"
const [valueChange, setValue] = createSignal<string>()
const value$ = state(valueChange, "")
const valueDecoder$ = combineLatest([selectedEntry$, dynamicBuilder$]).pipe(
map(([selectedEntry, builder]) =>
selectedEntry ? builder.buildDefinition(selectedEntry.value).dec : null,
const valueDecoder$ = combineLatest([selectedEntry$, selectedBlock$]).pipe(
map(([selectedEntry, { ctx }]) =>
selectedEntry
? ctx.dynamicBuilder.buildDefinition(selectedEntry.value).dec
: null,
),
)
const decodedValue$ = state(
@@ -34,8 +36,8 @@ export const StorageDecode: FC = () => {
const decoded = useStateObservable(decodedValue$)
const submit = async () => {
const [entry, ctx] = await firstValueFrom(
combineLatest([selectedEntry$, runtimeCtx$]),
const [entry, { ctx }] = await firstValueFrom(
combineLatest([selectedEntry$, selectedBlock$]),
)
addStorageSubscription({
@@ -46,6 +48,7 @@ export const StorageDecode: FC = () => {
type: entry!.value,
payload: decoded,
ctx: ctx!,
hash: null,
}),
})
}
+94 -66
View File
@@ -5,8 +5,6 @@ import { CopyText } from "@/components/Copy"
import SliderToggle from "@/components/Toggle"
import {
chainClient$,
dynamicBuilder$,
runtimeCtx$,
runtimeCtxAt$,
unsafeApi$,
} from "@/state/chains/chain.state"
@@ -23,6 +21,7 @@ import { Binary } from "polkadot-api"
import { FC } from "react"
import {
combineLatest,
distinctUntilChanged,
filter,
firstValueFrom,
from,
@@ -32,9 +31,11 @@ import {
scan,
startWith,
switchMap,
take,
withLatestFrom,
} from "rxjs"
import { twMerge } from "tailwind-merge"
import { selectedBlock$ } from "./BlockPicker"
import {
addStorageSubscription,
selectedEntry$,
@@ -49,74 +50,97 @@ export const StorageQuery: FC = () => {
if (!selectedEntry) return null
const submit = async () => {
const [entry, unsafeApi, keyValues, keysEnabled] = await firstValueFrom(
combineLatest([selectedEntry$, unsafeApi$, keyValues$, keysEnabled$]),
)
const [entry, unsafeApi, keyValues, keysEnabled, { hash }] =
await firstValueFrom(
combineLatest([
selectedEntry$,
unsafeApi$,
keyValues$,
keysEnabled$,
selectedBlock$,
]),
)
const args = keyValues.slice(0, keysEnabled)
const storageEntry = unsafeApi.query[entry!.pallet][entry!.entry]
const single = keyValues.length === keysEnabled
const argString = [...args.map(stringifyArg), ...(single ? [] : ["…"])]
addStorageSubscription({
name: `${entry!.pallet}.${entry!.entry}(${argString})`,
args,
single,
keyCodec: (hash) =>
runtimeCtxAt$(hash).pipe(
map(
(ctx) =>
ctx.dynamicBuilder.buildStorage(entry!.pallet, entry!.entry).keys,
),
),
at: (hash) => {
const value$ = from(
single
? storageEntry.getValue(...args, {
at: hash,
})
: storageEntry.getEntries(...args, {
at: hash,
}),
)
const hash$ = single
? chainClient$.pipe(
switchMap(({ chainHead }) =>
chainHead.storage$(hash, "hash", (ctx) =>
ctx.dynamicBuilder
.buildStorage(entry!.pallet, entry!.entry)
.keys.enc(...args),
),
const at = (hash: string) => {
const value$ = from(
single
? storageEntry.getValue(...args, {
at: hash,
})
: storageEntry.getEntries(...args, {
at: hash,
}),
)
const hash$ = single
? chainClient$.pipe(
switchMap(({ chainHead }) =>
chainHead.storage$(hash, "hash", (ctx) =>
ctx.dynamicBuilder
.buildStorage(entry!.pallet, entry!.entry)
.keys.enc(...args),
),
),
)
: of(null)
const ctxType$ = runtimeCtxAt$(hash).pipe(
map((ctx) => {
const pallet = ctx.lookup.metadata.pallets.find(
(p) => p.name === entry!.pallet,
)
const ctxEntry = pallet?.storage?.items.find(
(it) => it.name === entry!.entry,
)
if (!ctxEntry) {
throw new Error(
`Storage entry ${entry?.pallet}.${entry?.entry} not found in ${hash}`,
)
: of(null)
const ctxType$ = runtimeCtxAt$(hash).pipe(
map((ctx) => {
const pallet = ctx.lookup.metadata.pallets.find(
(p) => p.name === entry!.pallet,
)
const ctxEntry = pallet?.storage?.items.find(
(it) => it.name === entry!.entry,
)
if (!ctxEntry) {
throw new Error(
`Storage entry ${entry?.pallet}.${entry?.entry} not found in ${hash}`,
)
}
const type =
ctxEntry.type.tag === "plain"
? ctxEntry.type.value
: ctxEntry.type.value.value
}
const type =
ctxEntry.type.tag === "plain"
? ctxEntry.type.value
: ctxEntry.type.value.value
return { ctx, type }
}),
)
return { ctx, type }
}),
)
return combineLatest([
combineLatest({ value: value$, hash: hash$ }),
ctxType$,
]).pipe(map(([a, b]) => ({ ...a, ...b })))
},
})
return combineLatest([
combineLatest({ payload: value$, hash: hash$ }),
ctxType$,
]).pipe(
map(([a, b]) => ({ ...a, ...b })),
take(1),
)
}
const keyCodec = (hash: string) =>
runtimeCtxAt$(hash).pipe(
map(
(ctx) =>
ctx.dynamicBuilder.buildStorage(entry!.pallet, entry!.entry).keys,
),
)
if (hash) {
addStorageSubscription({
name: `${entry!.pallet}.${entry!.entry}(${argString})`,
args,
single,
keyCodec,
value: at(hash),
})
} else {
addStorageSubscription({
name: `${entry!.pallet}.${entry!.entry}(${argString})`,
args,
single,
keyCodec,
at,
})
}
}
return (
@@ -134,6 +158,7 @@ const keys$ = selectedEntry$.pipeState(
filter((e) => !!e),
map((entry) => entry.key),
withDefault([] as number[]),
distinctUntilChanged((a, b) => a.join(",") === b.join(",")),
)
const hashers$ = selectedEntry$.pipeState(
@@ -222,8 +247,8 @@ export const StorageKeysInput: FC<{
}
const builderState$ = state(
runtimeCtx$.pipe(
map((ctx) => ({
selectedBlock$.pipe(
map(({ ctx }) => ({
...ctx.dynamicBuilder,
lookup: ctx.lookup,
})),
@@ -356,10 +381,13 @@ const StorageKeyInput: FC<{
}
const keyCodec$ = state(
combineLatest([dynamicBuilder$, selectedEntry$]).pipe(
map(([builder, selectedEntry]) =>
combineLatest([selectedBlock$, selectedEntry$]).pipe(
map(([{ ctx }, selectedEntry]) =>
selectedEntry
? builder.buildStorage(selectedEntry.pallet, selectedEntry.entry).keys
? ctx.dynamicBuilder.buildStorage(
selectedEntry.pallet,
selectedEntry.entry,
).keys
: null,
),
),
+84 -49
View File
@@ -10,24 +10,26 @@ import { RuntimeContext } from "@polkadot-api/observable-client"
import { CodecComponentType, NOTIN } from "@polkadot-api/react-builder"
import { Button } from "@polkahub/ui-components"
import { useStateObservable } from "@react-rxjs/core"
import {
ChevronLeft,
ChevronRight,
PauseCircle,
PlayCircle,
Trash2,
} from "lucide-react"
import { ChevronLeft, ChevronRight, StopCircle, Trash2 } from "lucide-react"
import { Binary, Enum } from "polkadot-api"
import { FC, ReactNode, useMemo, useState } from "react"
import { Virtuoso } from "react-virtuoso"
import { BlockState } from "../Explorer/block.state"
import { BlockStatusIcon } from "../Explorer/Detail/BlockState"
import {
KeyCodec,
removeStorageSubscription,
stopStorageSubscription,
storageSubscription$,
storageSubscriptionKeys$,
StorageSubscriptionValue,
toggleSubscriptionPause,
} from "./storage.state"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Link } from "@/hashParams"
export const StorageSubscriptions: FC = () => {
const keys = useStateObservable(storageSubscriptionKeys$)
@@ -77,10 +79,22 @@ const ValueSubscriptionBox: FC<{ subscription: string }> = ({
if (!storageSubscription) return null
const status = storageSubscription.status
if (status.type !== "value") return null
const { ctx, payload, type } = status.value
const { ctx, payload, type, hash } = status.value
return (
<SubscriptionBox subscription={subscription}>
<SubscriptionBox
subscription={subscription}
actions={
hash ? (
<div className="text-center">
<p className="text-sm">Hash</p>
<Link to={`/explorer/${hash}`} className="underline">
<p className="font-mono text-xs">{shortStr(hash, 6)}</p>
</Link>
</div>
) : null
}
>
{(mode) => (
<ResultDisplay
id={subscription}
@@ -88,7 +102,7 @@ const ValueSubscriptionBox: FC<{ subscription: string }> = ({
result: Enum("success", {
ctx,
type,
value: payload,
payload: payload,
hash: null,
}),
}}
@@ -126,32 +140,51 @@ const ValuesSubscriptionBox: FC<{ subscription: string }> = ({
<SubscriptionBox
subscription={subscription}
actions={
<div className="flex items-center gap-1">
<Button
disabled={!hasPrev}
onClick={() => setTarget(status.value[targetValueIdx - 1].height)}
>
<ChevronLeft />
</Button>
<div className="text-xs text-center">
<p>{targetValue?.height}</p>
<p className="font-mono">
{targetValue ? shortStr(targetValue.blockHash, 6) : null}
</p>
targetValue ? (
<div className="flex items-center gap-1">
<Button
variant="secondary"
className="has-[>svg]:px-1"
disabled={!hasPrev}
onClick={() => setTarget(status.value[targetValueIdx - 1].height)}
>
<ChevronLeft />
</Button>
<div className="text-center">
<div className="text-sm flex items-center gap-1 justify-center">
<p>{targetValue.height}</p>
<BlockStatusIcon
size={20}
state={
targetValue.settled ? BlockState.Finalized : BlockState.Best
}
/>
</div>
<Link
to={`/explorer/${targetValue.blockHash}`}
className="underline"
>
<p className="font-mono text-xs">
{shortStr(targetValue.blockHash, 6)}
</p>
</Link>
</div>
<Button
disabled={!hasNext}
variant="secondary"
className="has-[>svg]:px-1"
onClick={() =>
setTarget(
targetValueIdx === status.value.length - 2
? "best"
: status.value[targetValueIdx + 1].height,
)
}
>
<ChevronRight />
</Button>
</div>
<Button
disabled={!hasNext}
onClick={() =>
setTarget(
targetValueIdx === status.value.length - 2
? "best"
: status.value[targetValueIdx + 1].height,
)
}
>
<ChevronRight />
</Button>
</div>
) : null
}
>
{(mode) =>
@@ -207,18 +240,20 @@ const SubscriptionBox: FC<{
},
]}
/>
{storageSubscription.completed ? null : (
<button onClick={() => toggleSubscriptionPause(subscription)}>
{storageSubscription.paused ? (
<PlayCircle {...iconButtonProps} />
) : (
<PauseCircle {...iconButtonProps} />
)}
{storageSubscription.completed ? (
<button onClick={() => removeStorageSubscription(subscription)}>
<Trash2 {...iconButtonProps} />
</button>
) : (
<Tooltip>
<TooltipTrigger asChild>
<button onClick={() => stopStorageSubscription(subscription)}>
<StopCircle {...iconButtonProps} />
</button>
</TooltipTrigger>
<TooltipContent>Stop subscription</TooltipContent>
</Tooltip>
)}
<button onClick={() => removeStorageSubscription(subscription)}>
<Trash2 {...iconButtonProps} />
</button>
</div>
</div>
{children(mode)}
@@ -249,7 +284,7 @@ const DecodedResultDisplay: FC<{
)
}
const { value, ctx, type } = subValue.result.value
const { payload: value, ctx, type } = subValue.result.value
if (single) {
return (
@@ -267,7 +302,7 @@ const DecodedResultDisplay: FC<{
)
}
const values = subValue.result.value.value as Array<{
const values = subValue.result.value.payload as Array<{
keyArgs: unknown[]
value: unknown
}>
@@ -321,7 +356,7 @@ const JsonResultDisplay: FC<{
}
return (
<div className="max-h-[60svh] overflow-auto">
<JsonDisplay src={subValue.result.value.value} />
<JsonDisplay src={subValue.result.value.payload} />
</div>
)
}
+17 -33
View File
@@ -1,10 +1,6 @@
import { bytesToString } from "@/components/BinaryInput"
import { getHashParams } from "@/hashParams"
import {
client$,
runtimeCtx$,
selectedChainChanged$,
} from "@/state/chains/chain.state"
import { client$, selectedChainChanged$ } from "@/state/chains/chain.state"
import {
BlockInfo,
concatMapEager,
@@ -23,7 +19,6 @@ import {
catchError,
combineLatest,
concat,
distinctUntilChanged,
EMPTY,
filter,
ignoreElements,
@@ -41,6 +36,7 @@ import {
withLatestFrom,
} from "rxjs"
import { v4 as uuid } from "uuid"
import { selectedBlock$ } from "./BlockPicker"
export type StorageMetadataEntry = {
pallet: string
@@ -56,8 +52,8 @@ export const [entryChange$, selectEntry] = createSignal<{
entry?: string | null
}>()
const palletEntries$ = runtimeCtx$.pipe(
map((ctx) =>
const palletEntries$ = selectedBlock$.pipe(
map(({ ctx }) =>
Object.fromEntries(
ctx.lookup.metadata.pallets.map((p) => [p.name, p.storage?.items ?? []]),
),
@@ -104,9 +100,9 @@ export const partialEntry$ = state(
)
export const selectedEntry$ = state(
partialEntry$.pipe(
withLatestFrom(palletEntries$, runtimeCtx$),
map(([partialEntry, entries, ctx]): StorageMetadataEntry | null => {
combineLatest([partialEntry$, selectedBlock$]).pipe(
withLatestFrom(palletEntries$),
map(([[partialEntry, { ctx }], entries]): StorageMetadataEntry | null => {
const entry = partialEntry.pallet
? entries[partialEntry.pallet]?.find(
(v) => v.name === partialEntry.entry,
@@ -176,17 +172,18 @@ export const [newStorageSubscription$, addStorageSubscription] = createSignal<{
type: number
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
hash: HexString | null
value: unknown
payload: unknown
}>
value?: Observable<{
type: number
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
hash: string | null
payload: unknown
}>
}>()
export const [removeStorageSubscription$, removeStorageSubscription] =
createKeyedSignal<string>()
export const [togglePause$, toggleSubscriptionPause] =
export const [stopStorageSubscription$, stopStorageSubscription] =
createKeyedSignal<string>()
export type StorageSubscriptionValue = {
@@ -199,7 +196,7 @@ export type StorageSubscriptionValue = {
hash: string | null
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
type: number
value: unknown
payload: unknown
}
error: string
}>
@@ -208,11 +205,11 @@ export type StorageSubscription = {
name: string
args: unknown[] | null
single: boolean
paused: boolean
completed: boolean
status: Enum<{
loading: undefined
value: {
hash: string | null
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
type: number
payload: unknown
@@ -226,7 +223,7 @@ const getStatus$ = (
type: number
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
hash: HexString | null
value: unknown
payload: unknown
}>,
single: boolean,
keyCodec?: (hash: HexString) => Observable<KeyCodec>,
@@ -362,10 +359,6 @@ const [getStorageSubscription$, storageSubscriptionKeyChange$] = partitionByKey(
keyCodec,
...props
}): Observable<StorageSubscription> => {
const paused$ = togglePause$(id).pipe(
scan((v) => !v, false),
startWith(false),
)
const status$: Observable<StorageSubscription["status"]> = value
? value.pipe(map((v) => Enum("value", v)))
: getStatus$(at!, props.single, keyCodec)
@@ -379,16 +372,16 @@ const [getStorageSubscription$, storageSubscriptionKeyChange$] = partitionByKey(
status: Enum("loading"),
}),
shareReplay(1),
takeUntil(stopStorageSubscription$(id)),
)
const completed$ = concat(
of(false),
result$.pipe(ignoreElements()),
of(true),
)
return combineLatest([paused$, completed$, result$]).pipe(
map(([paused, completed, result]) => ({
return combineLatest([completed$, result$]).pipe(
map(([completed, result]) => ({
...result,
paused,
completed,
})),
)
@@ -409,16 +402,7 @@ export const storage$ = storageSubscriptionKeys$
export const storageSubscription$ = state(
(key: string): Observable<StorageSubscription> =>
getStorageSubscription$(key).pipe(
// Don't propagate if paused
distinctUntilChanged((prev, current) => {
// if it's not paused, mark it as "not equal" for the update to go through
if (!current.paused) return false
// otherwise, don't propagate if we were previously paused
// but do propagate if we weren't: Because we still need to show the "paused" status.
return prev.paused === current.paused
}),
),
getStorageSubscription$(key),
null,
)